Slide 1

Slide 1 text

Beyond Modularization: Scaling Android Builds Nelson Osacky

Slide 2

Slide 2 text

Me • Previously Android Engineer • Large projects • SoundCloud • Square • Small startups • Solutions Engineer at Gradle

Slide 3

Slide 3 text

Me • Gradle Plugin Maintainer • Fladle - Easily Scale Instrumentation Tests on Firebase https://github.com/runningcode/fladle • Delect - Replace Dagger with Dagger Reflect https://github.com/soundcloud/delect/ • Gradle Doctor - Actionable Insights for your build https://github.com/runningcode/gradle-doctor

Slide 4

Slide 4 text

Solutions Engineer https://gradle.com/enterprise/trial/

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Gradle Build Tool Gradle Enteprise

Slide 7

Slide 7 text

Android Gradle Plugin Android Studio

Slide 8

Slide 8 text

Kotlin Intellij

Slide 9

Slide 9 text

Android Development Feature Development Tech Debt (There are other things too)

Slide 10

Slide 10 text

Tech Debt Refactoring Build Speed

Slide 11

Slide 11 text

Build Speed is Tech Debt And it always pays off

Slide 12

Slide 12 text

Cost of Builds 60s waste * 50 builds / day * 50 devs = 42 hours lost / day

Slide 13

Slide 13 text

Cost of Builds 60s waste * 50 builds / day * 50 devs = 42 hours lost / day not including lost focus https://gradle.com/roi-calculator

Slide 14

Slide 14 text

Fast Builds Matter

Slide 15

Slide 15 text

Cost of Builds 60s waste * 50 builds / day * 50 devs = 42 hours lost / day hire 5 new people without paying them! no recruiting https://gradle.com/roi-calculator

Slide 16

Slide 16 text

Build Speed is Tech Debt And it always pays off And is easy to justify working on it

Slide 17

Slide 17 text

Motivated? Where do we start?

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

Update All The Things ⬢ Gradle 6.5 ⬢ Android Gradle Plugin 4.0.0 ⬢ Gradle Enterprise Plugin 3.3.4 ⬢ Kotlin 1.3.72 ⬢ Third party plugins ⬢ Third party libraries New Performance APIs Caching Improvements Task Configuration Avoidance Background Scan Uploads Incremental Annotation Processors Compiler Perf Improvements

Slide 20

Slide 20 text

Update All The Things plugins { id "com.github.ben-manes.versions" version "0.28.0" } ./gradlew dependencyUpdates -Drevision=release https://github.com/ben-manes/gradle-versions-plugin

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

Build Lifecycle •Initialization •Configuration •Execution

Slide 23

Slide 23 text

Build Lifecycle Initialization Sets up the environment for the build and determines which projects will take part in it.

Slide 24

Slide 24 text

Build Lifecycle Configuration All build scripts of all projects are executed. Constructs and configures the task graph.

Slide 25

Slide 25 text

Build Lifecycle Execution Runs the tasks graph in order.

Slide 26

Slide 26 text

Where to start?

Slide 27

Slide 27 text

Check for Red Flags

Slide 28

Slide 28 text

What is a red flag?

Slide 29

Slide 29 text

If nothing changes, no tasks should execute.

Slide 30

Slide 30 text

./gradlew assembleDebug ./gradlew assembleDebug BUILD SUCCESSFUL in 2s 58 actionable tasks: 58 up-to-date

Slide 31

Slide 31 text

./gradlew assembleDebug ./gradlew assembleDebug BUILD SUCCESSFUL in 5s 359 actionable tasks: 3 executed, 356 from cache

Slide 32

Slide 32 text

Check for Red Flags Regularly

Slide 33

Slide 33 text

BUILD SUCCESSFUL in 5s 359 actionable tasks: 3 executed, 356 up-to-date Publishing build scan... https://gradle.com/s/yj5jtd4kh6ucc Build Analyzer results available

Slide 34

Slide 34 text

BUILD SUCCESSFUL in 5s 359 actionable tasks: 3 executed, 356 up-to-date Publishing build scan... https://gradle.com/s/yj5jtd4kh6ucc Build Analyzer results available Gradle Build Scans Android Studio Build Analyzer

Slide 35

Slide 35 text

Android Studio Build Analyzer

Slide 36

Slide 36 text

Run a build scan ./gradlew assembleDebug --scan https://scans.gradle.com/

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

Filter

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

9 tasks ran Bugsnag

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

#perfmatters

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

1 sec per project is high

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

25 sec is red flag!

Slide 50

Slide 50 text

apply plugin: 'witness'

Slide 51

Slide 51 text

//apply plugin: 'witness'

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

5.8s to 0.5s by disabling two plugins!

Slide 54

Slide 54 text

Configuration time savings affect every build

Slide 55

Slide 55 text

Configuration time should be roughly 5s per 100 projects

Slide 56

Slide 56 text

Disable plugins for local builds

Slide 57

Slide 57 text

engBuild pattern

Slide 58

Slide 58 text

engBuild != buildVariant

Slide 59

Slide 59 text

engBuild == Gradle property https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties

Slide 60

Slide 60 text

enabled by default ext.engBuild = (project.findProperty('engBuild') ?: 'true') .toBoolean()

Slide 61

Slide 61 text

if (!engBuild) { apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.firebase-perf' }

Slide 62

Slide 62 text

./gradlew assembleDebug ./gradlew assembleRelease -PengBuild=false disable on CI or testing release builds

Slide 63

Slide 63 text

engBuild=false disable on CI or testing release builds gradle.properties

Slide 64

Slide 64 text

engBuild pattern

Slide 65

Slide 65 text

16627 tasks is a red flag

Slide 66

Slide 66 text

Use Task Configuration Avoidance https://docs.gradle.org/current/userguide/task_configuration_avoidance.html

Slide 67

Slide 67 text

api vs implementation

Slide 68

Slide 68 text

Module A Module B Module C dependencies { implementation project('module-b') implementation project('module-c') } dependencies { implementation project('module-c') }

Slide 69

Slide 69 text

Module A Module B Module C dependencies { implementation project('module-b') } dependencies { api project('module-c') } module-c must always used when consuming module-b

Slide 70

Slide 70 text

api vs implementation convenience vs performance

Slide 71

Slide 71 text

Real World Example

Slide 72

Slide 72 text

https://github.com/k9mail/k-9 K9 Mail App

Slide 73

Slide 73 text

K9 Mail App https://github.com/k9mail/k-9/blob/master/app/core/build.gradle#L12 apiaproject(':plugins:openpgp-api-lib:openpgp-api')

Slide 74

Slide 74 text

K9 Mail App https://github.com/k9mail/k-9/blob/master/app/core/build.gradle#L12 apiaproject(':plugins:openpgp-api-lib:openpgp-api')

Slide 75

Slide 75 text

apiaproject(':plugins:openpgp-api-lib:openpgp-api') K9 Mail App

Slide 76

Slide 76 text

if (project.hasProperty('useImpl')) { implementation project(':plugins:openpgp-api-lib:openpgp-api') } else { apiaproject(':plugins:openpgp-api-lib:openpgp-api') } K9 Mail App

Slide 77

Slide 77 text

./gradlew :app:k9mail:assembleDebug ./gradlew :app:k9mail:assembleDebug -PuseImpl K9 Mail App

Slide 78

Slide 78 text

Profile it!

Slide 79

Slide 79 text

https://github.com/gradle/gradle-profiler Gradle Profiler

Slide 80

Slide 80 text

Let's write scenarios Gradle Profiler

Slide 81

Slide 81 text

change_api_dep { } performance.scenarios Gradle Profiler

Slide 82

Slide 82 text

change_api_dep { title = "Change using api dependency" } performance.scenarios Gradle Profiler

Slide 83

Slide 83 text

change_api_dep { title = "Change using api dependency" tasks = ["app:k9mail:assembleDebug"] } performance.scenarios Gradle Profiler

Slide 84

Slide 84 text

change_api_dep { title = "Change using api dependency" tasks = ["app:k9mail:assembleDebug"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/ openintents/openpgp/OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 85

Slide 85 text

change_api_dep { title = "Change using api dependency" tasks = ["app:k9mail:assembleDebug"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/ openintents/openpgp/OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 86

Slide 86 text

change_api_dep { title = "Change using api dependency" tasks = ["app:k9mail:assembleDebug"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/openintents/openpgp/ OpenPgpApiManager.java" } change_impl_dep { title = "Change using impl dependency" tasks = ["app:k9mail:assembleDebug"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/openintents/openpgp/ OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 87

Slide 87 text

change_impl_dep { title = "Change using impl dependency" tasks = ["app:k9mail:assembleDebug"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/ openintents/openpgp/OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 88

Slide 88 text

change_impl_dep { title = "Change using impl dependency" tasks = ["app:k9mail:assembleDebug"] gradle-args = ["-PuseImpl"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/ openintents/openpgp/OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 89

Slide 89 text

change_impl_dep { title = "Change using impl dependency" tasks = ["app:k9mail:assembleDebug"] gradle-args = ["-PuseImpl"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/ openintents/openpgp/OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 90

Slide 90 text

change_api_dep { title = "Change using api dependency" tasks = ["app:k9mail:assembleDebug"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/openintents/openpgp/ OpenPgpApiManager.java" } change_impl_dep { title = "Change using impl dependency" tasks = ["app:k9mail:assembleDebug"] gradle-args = ["-PuseImpl"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/openintents/openpgp/ OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 91

Slide 91 text

change_api_dep { title = "Change using api dependency" tasks = ["app:k9mail:assembleDebug"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/openintents/openpgp/ OpenPgpApiManager.java" } change_impl_dep { title = "Change using impl dependency" tasks = ["app:k9mail:assembleDebug"] gradle-args = ["-PuseImpl"] apply-non-abi-change-to = "openpgp-api/src/main/java/org/openintents/openpgp/ OpenPgpApiManager.java" } performance.scenarios Gradle Profiler

Slide 92

Slide 92 text

gradle-profiler --benchmark --scenario-file performance.scenarios Gradle Profiler

Slide 93

Slide 93 text

1.stop daemon 2.clean 3.6 warm up builds 4.10 benchmark builds Gradle Profiler

Slide 94

Slide 94 text

No content

Slide 95

Slide 95 text

html report and csv report

Slide 96

Slide 96 text

Build Time (milliseconds) 0 3250 6500 9750 13000 #1 #2 #3 #4 #5 #6 #7 #8 #9 #10 Change using api dependency Change using impl dependency

Slide 97

Slide 97 text

scenario Change using api dependency Change using impl dependency mean 11603.0 10151.7 median 11529.5 10049.5 confidence 0.0 99.98429477157690

Slide 98

Slide 98 text

~15% improvement

Slide 99

Slide 99 text

why?

Slide 100

Slide 100 text

gradle-profiler --profile buildscan --scenario-file performance.scenarios

Slide 101

Slide 101 text

gradle-profiler --profile buildscan --scenario-file performance.scenarios * Results written to /Users/no/workspace/k-9/profile-out-14 Scenario Change using api dependency using Gradle 6.5 - Build scan for measured build #1: https://gradle.com/s/ozev5czodllsk Scenario Change using impl dependency using Gradle 6.5 - Build scan for measured build #1: https://gradle.com/s/rxujhuyxqlybu

Slide 102

Slide 102 text

https://scans.gradle.com/s/ozev5czodllsk/timeline?outcomeFilter=SUCCESS api Dependency

Slide 103

Slide 103 text

https://scans.gradle.com/s/rxujhuyxqlybu/timeline?outcomeFilter=SUCCESS implementation Dependency

Slide 104

Slide 104 text

Which tasks ran?

Slide 105

Slide 105 text

https://scans.gradle.com/s/ozev5czodllsk/timeline?outcomeFilter=SUCCESS api Dependency

Slide 106

Slide 106 text

https://scans.gradle.com/s/rxujhuyxqlybu/timeline?outcomeFilter=SUCCESS implementation Dependency

Slide 107

Slide 107 text

https://scans.gradle.com/s/ozev5czodllsk/timeline?outcomeFilter=SUCCESS api Dependency

Slide 108

Slide 108 text

https://scans.gradle.com/s/ozev5czodllsk/timeline?outcomeFilter=SUCCESS api Dependency

Slide 109

Slide 109 text

What exactly is the difference?

Slide 110

Slide 110 text

Enterprise Feature Alert

Slide 111

Slide 111 text

Capture Task Input Files https://docs.gradle.com/enterprise/gradle-plugin/#capturing_task_input_files

Slide 112

Slide 112 text

gradleEnterprise { buildScan { captureTaskInputFiles = true } } https://docs.gradle.com/enterprise/gradle-plugin/#capturing_task_input_files

Slide 113

Slide 113 text

Build Scan Comparison

Slide 114

Slide 114 text

Build Scan Comparison

Slide 115

Slide 115 text

Build Scan Comparison Build B uses api

Slide 116

Slide 116 text

Adding unnecessary inputs to tasks.

Slide 117

Slide 117 text

How do we fix it?

Slide 118

Slide 118 text

https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin Dependency Analysis Plugin plugins { id "com.autonomousapps.dependency-analysis" version "0.49.0" }

Slide 119

Slide 119 text

No content

Slide 120

Slide 120 text

Annotation Processors

Slide 121

Slide 121 text

Make sure they are incremental!

Slide 122

Slide 122 text

w: [kapt] Incremental annotation processing requested, but support is disabled because the following processors are not incremental: androidx.room.RoomProcessor (NON_INCREMENTAL).

Slide 123

Slide 123 text

android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments += [ "room.schemaLocation":"$projectDir/schemas".toString(), "room.incremental":"true"] } } } }

Slide 124

Slide 124 text

Room

Slide 125

Slide 125 text

android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments += [ "room.schemaLocation":"$projectDir/schemas".toString(), "room.incremental":"true"] } } } }

Slide 126

Slide 126 text

"room.schemaLocation":"$projectDir/schemas".toString(), Remote Cache miss!

Slide 127

Slide 127 text

"room.schemaLocation":"$projectDir/schemas".toString(), Remote Cache miss! https://developers.soundcloud.com/blog/gradle-remote-build-cache-misses

Slide 128

Slide 128 text

Move Room to it's own Room https://github.com/gradle/android-cache-fix-gradle-plugin Module

Slide 129

Slide 129 text

No content

Slide 130

Slide 130 text

Disabling unused AGP Features

Slide 131

Slide 131 text

Cost of Generating BuildConfig All 186 are UP-TO-DATE.

Slide 132

Slide 132 text

Unused so Proguard will remove it later

Slide 133

Slide 133 text

BuildConfig adds java sources to pure kotlin modules

Slide 134

Slide 134 text

Which BuildConfig?

Slide 135

Slide 135 text

Which version code?

Slide 136

Slide 136 text

Which version code?

Slide 137

Slide 137 text

VERSION_CODE is 1 by default in library modules

Slide 138

Slide 138 text

Aidl what?

Slide 139

Slide 139 text

Also adds task configuration time.

Slide 140

Slide 140 text

No content

Slide 141

Slide 141 text

android.defaults.buildfeatures.buildconfig=false android.defaults.buildfeatures.aidl=false android.defaults.buildfeatures.renderscript=false android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false gradle.properties New in AGP 4.0.0 https://developer.android.com/studio/releases/gradle-plugin#buildFeatures

Slide 142

Slide 142 text

Disable for all projects

Slide 143

Slide 143 text

Enable when needed

Slide 144

Slide 144 text

android.buildFeatures { buildConfig = true } app/build.gradle

Slide 145

Slide 145 text

Code Multipliers

Slide 146

Slide 146 text

AndroidX Jetifier

Slide 147

Slide 147 text

Migrates support-library- dependent libraries to rely on Android X packages.

Slide 148

Slide 148 text

android.enableJetifier=true

Slide 149

Slide 149 text

No content

Slide 150

Slide 150 text

android.enableJetifier=false

Slide 151

Slide 151 text

Dependency Substitution https://issuetracker.google.com/issues/156449751

Slide 152

Slide 152 text

Stuck on the Jetifier?

Slide 153

Slide 153 text

plugins { id "com.github.plnice.canidropjetifier" version "0.5" } Can I drop Jetifier? ./gradlew -Pandroid.enableJetifier=false canIDropJetifier

Slide 154

Slide 154 text

Jetify At Home ./jetifier-standalone -i -o https://developer.android.com/studio/command-line/jetifier

Slide 155

Slide 155 text

Jetify At Home Upload to nexus

Slide 156

Slide 156 text

Flavors

Slide 157

Slide 157 text

Flavor Explosion

Slide 158

Slide 158 text

No content

Slide 159

Slide 159 text

Flavor Explosion When you add a build dimension multiplying build speed in a surprising surprise.

Slide 160

Slide 160 text

Flavor Explosion BuildType * BuildFlavor = BuildVariants

Slide 161

Slide 161 text

Flavor Explosion BuildType * BuildFlavor = BuildVariants BuildVariant are code multipliers

Slide 162

Slide 162 text

Flavor Explosion 2 BuildTypes * 3 BuildFlavors = 6 Variants 6x code 6x tasks 6x resources 6x cache utilization

Slide 163

Slide 163 text

def prop = project.findProperty("enabledBuildVariant") ?: "freeDebug" android.variantFilter { variant -> if (variant.name != prop) { variant.ignore = true } } ./gradlew assemblePaidDebug -PenableBuildVariant=paidDebug

Slide 164

Slide 164 text

Flavor Explosion • Mostly a configuration time win. • Permanently disable flavors you don't need.

Slide 165

Slide 165 text

How to cut build times in half

Slide 166

Slide 166 text

How to cut build times in half.

Slide 167

Slide 167 text

apply plugin: 'com.android.library'

Slide 168

Slide 168 text

assembleDebug assembleRelease

Slide 169

Slide 169 text

2x code 2x resources 2x tasks 2x tests !!!!!

Slide 170

Slide 170 text

How often does the debug and release code differ for library modules?

Slide 171

Slide 171 text

./gradlew test runs all tests twice

Slide 172

Slide 172 text

./gradlew testDebug java module tests don't run

Slide 173

Slide 173 text

Recompile everything when switching to release

Slide 174

Slide 174 text

CI takes twice as long

Slide 175

Slide 175 text

if (engBuild) { } Disable unused flavors in library modules

Slide 176

Slide 176 text

if (engBuild) { android.variantFilter { variant -> } } Disable unused flavors in library modules

Slide 177

Slide 177 text

if (engBuild) { android.variantFilter { variant -> if (variant.name == 'release') { variant.ignore = true } } } Disable unused flavors in library modules

Slide 178

Slide 178 text

if (engBuild) { android.variantFilter { variant -> if (variant.name == 'release') { variant.ignore = true } } } Disable unused flavors in library modules

Slide 179

Slide 179 text

if (engBuild) { android.variantFilter { variant -> if (variant.name == 'release') { variant.ignore = true } } } Disable unused flavors in library modules

Slide 180

Slide 180 text

if (engBuild) { android.variantFilter { variant -> if (variant.name == 'debug') { variant.ignore = true } } } Disable unused flavors in library modules

Slide 181

Slide 181 text

android.variantFilter { variant -> if (variant.name == 'debug') { variant.ignore = true } } Disable unused flavors in library modules android.debug.matchingFallbacks = ['release'] in application module

Slide 182

Slide 182 text

android.variantFilter { variant -> if (variant.name == 'debug') { variant.ignore = true } } Disable unused flavors in library modules

Slide 183

Slide 183 text

Build and test times cut in half

Slide 184

Slide 184 text

Switching to release build doesn't recompile everything

Slide 185

Slide 185 text

Disable stuff! #protip

Slide 186

Slide 186 text

Disable stuff! #protip measure!

Slide 187

Slide 187 text

./gradlew help Measure configuration time

Slide 188

Slide 188 text

Disable debug variant Disable debug variant and AGP features Disable AGP features Standard build 0.0 200.0 400.0 600.0 800.0 Avg Configuration Time (ms) Average of 10 builds help using Gradle Profiler

Slide 189

Slide 189 text

783ms to 688ms ~= 13% faster

Slide 190

Slide 190 text

13% configuration time improvement that scales

Slide 191

Slide 191 text

Regular build assembleDebug

Slide 192

Slide 192 text

assembleDebug Debug disabled, AGP features disabled

Slide 193

Slide 193 text

Disable More Things https://github.com/android/gradle-recipes

Slide 194

Slide 194 text

engBuild Ideas

Slide 195

Slide 195 text

engBuild Ideas •Higher minSdk •Disables Multidex •Disable coreLibraryDesugaring •Strip other languages •Strip large images •Strip out native libraries •Try new versions of AGP https://medium.com/@runningcode/testing-new-versions-of-the-android-gradle-plugin-ea80df978316

Slide 196

Slide 196 text

Always Measure!

Slide 197

Slide 197 text

Upcoming Performance Enhancements

Slide 198

Slide 198 text

File System Watching

Slide 199

Slide 199 text

Fast input snapshotting

Slide 200

Slide 200 text

File System Watching https://blog.gradle.org/introducing-file-system-watching org.gradle.unsafe.watch-fs=true

Slide 201

Slide 201 text

Configuration Caching

Slide 202

Slide 202 text

Configuration Caching

Slide 203

Slide 203 text

Configuration Caching The build scripts of all projects which are part of the build are executed.

Slide 204

Slide 204 text

ext.engBuild = (project.findProperty('engBuild') ?: 'true') .toBoolean()

Slide 205

Slide 205 text

ext.engBuild = providers.gradleProperty("engBuild") .forUseAtConfigurationTime() .orElse("true") .map { it.toBoolean() }

Slide 206

Slide 206 text

if (!engBuild) { apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.firebase-perf' }

Slide 207

Slide 207 text

if (!engBuild.get()) { apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.firebase-perf' }

Slide 208

Slide 208 text

Improving Android Build Performance Devoxx Belgium 2018 Cédric Champeau https://www.youtube.com/watch?v=StahHZ-L83k https://speakerdeck.com/melix/improving-android-build-performance

Slide 209

Slide 209 text

Build Bigger, Better: Gradle for Large Projects Google I/O 2019 Aurimas Liutikas and Xavier Ducrohet https://www.youtube.com/watch?v=sQC9-Rj2yLI

Slide 210

Slide 210 text

The Secrets of the Build Scan Plugin and the Internals of Gradle Virtual Android Makers Paris 2020 Me https://www.youtube.com/watch?v=lgaqS0pmUzk

Slide 211

Slide 211 text

Measuring remote build cache performance https://medium.com/@runningcode https://github.com/runningcode/gradle-doctor

Slide 212

Slide 212 text

Thank you! Need advice? osacky.com https://gradle.com/enterprise/trial/ https://speakerdeck.com/runningcode/beyond-modularization-scaling-your-android-build-with-gradle