Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Agenda ● How Netflix does builds ○ Gradle / AGP / Android Studio / CI/CD ○ Eng Mode ○ Modularisation ○ Gradle profiler ● The Developer Productivity Journey ○ The first Build Scan™ ○ Build caching, who’s enabled it and who hasn’t? ○ Identifying build performance bottlenecks ○ Enabling local and remote build caching ● Build Performance Regressions: One year later ○ Android SDK and JDK version ○ CI vs Local cache-miss ○ Code generation ○ Dependencies management ● Wrap Up

Slide 3

Slide 3 text

How Netflix Does Builds Build Environment & Tools Kapt with Dagger/Hilt, Epoxy, Auto Value App Bundles Jetpack Compose Android Studio / AGP 7.2.1 / Kotlin 1.7.10 Gradle project with ~300 Modules and 10+ variants / flavors

Slide 4

Slide 4 text

SCM : Bitbucket/stash, CI : Jenkins, CD : Spinnaker Pull request with: ● Static analysis with ktlint, detekt, lint (custom lint) ● Unit testing and on-device testing ● 4.6K CI builds per week (220K per year) ● 2.3K local builds per week (110K per year) How Netflix Does Builds Workflow / CI / CD

Slide 5

Slide 5 text

A Day In the Life of an Android Developer at Netflix Commit to Publish ● Make change in a feature branch ● Open a pull request ● Wait for automatic code review ● Wait for peers to review ● Merge to main ● Included in next release train (gated if necessary)

Slide 6

Slide 6 text

A Day In the Life of an Android Developer at Netflix Commit to Publish ● Make change in a feature branch ● Open a pull request ● Wait for automatic code review ● Wait for peers to review ● Merge to main ● Included in next release train (gated if necessary)

Slide 7

Slide 7 text

A Day In the Life of an Android Developer at Netflix Commit to Publish ● Make change in a feature branch ● Open a pull request ● Wait for automatic code review ● Wait for peers to review ● Merge to main ● Included in next release train (gated if necessary)

Slide 8

Slide 8 text

Netflix Android Developer Productivity Wins What is Eng Build and how it helped make devs more productive

Slide 9

Slide 9 text

./gradlew :NetflixMainApplication:assembleDebug

Slide 10

Slide 10 text

./gradlew :NetflixMainApplication:assembleDebug BUILD SUCCESSFUL in 7m 11s 3512 actionable tasks: 3512 executed, 0 up-to-date

Slide 11

Slide 11 text

./gradlew :NetflixMainApplication:assembleDebug BUILD SUCCESSFUL in 7m 11s 3512 actionable tasks: 3512 executed, 0 up-to-date ./gradlew :NetflixMainApplication:assembleDebug

Slide 12

Slide 12 text

./gradlew :NetflixMainApplication:assembleDebug BUILD SUCCESSFUL in 7m 11s 3512 actionable tasks: 3512 executed, 0 up-to-date ./gradlew :NetflixMainApplication:assembleDebug BUILD SUCCESSFUL in 11s 3512 actionable tasks: 0 executed, 3512 up-to-date

Slide 13

Slide 13 text

./gradlew :NetflixMainApplication:assembleDebug BUILD SUCCESSFUL in 7m 11s 3512 actionable tasks: 3512 executed, 0 up-to-date ./gradlew :NetflixMainApplication:assembleDebug BUILD SUCCESSFUL in 2m 1s 3512 actionable tasks: 562 executed, 2950 up-to-date

Slide 14

Slide 14 text

def buildDate = getBuildDate() android { defaultConfig { buildConfigField "String", "BUILD_DATE", "\"$buildDate\"" } }

Slide 15

Slide 15 text

public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String LIBRARY_PACKAGE_NAME = "com.netflix… public static final String BUILD_TYPE = "debug"; -/ Field from default config. public static final String BUILD_DATE = "02082022-091312"; }

Slide 16

Slide 16 text

def buildDate = if (engBuild()) "N-A" else getBuildDate() android { defaultConfig { buildConfigField "String", "BUILD_DATE", "\"$buildDate\"" } }

Slide 17

Slide 17 text

Eng mode ● Optimize incremental compilation ● Remove unnecessary plugins ● Remove variants ● Remove app bundles

Slide 18

Slide 18 text

First Results of Remote Build Cache for Netflix CI CI build/test time has improved by 46%. Average CI build time is down from 14.6 min to 8 min. The remote cache is saving 6.6 min per CI build on aggregate for all CI builds. EngBuild assembleDebug build time: ~2 min versus assembleDebug build time of ~7 min

Slide 19

Slide 19 text

Netflix Android Developer Productivity Wins Why Modularize? - More usability - Speed up incremental builds - Improved caching and parallelism

Slide 20

Slide 20 text

Modularization - Monolithic app

Slide 21

Slide 21 text

Modularization - First modules

Slide 22

Slide 22 text

Modularization - First modules ✏ ✏ code change ⚙ need recompilation

Slide 23

Slide 23 text

Modularization - First modules ⚙ ✏ ⚙ ✏ code change ⚙ need recompilation

Slide 24

Slide 24 text

Modularization - First modules ✏ ✏ code change ⚙ need recompilation

Slide 25

Slide 25 text

Modularization - First modules ✏ ⚙ ⚙ ⚙ ⚙ ✏ code change ⚙ need recompilation

Slide 26

Slide 26 text

Modularization - Api / Impl

Slide 27

Slide 27 text

Modularization - Api / Impl

Slide 28

Slide 28 text

Modularization - Api / Impl ✏ ✏ code change ⚙ need recompilation

Slide 29

Slide 29 text

Modularization - Api / Impl ⚙ ✏ ⚙ ✏ code change ⚙ need recompilation

Slide 30

Slide 30 text

Modularization - Api / Impl ✏

Slide 31

Slide 31 text

Modularization - Api / Impl ✏ ⚙ ⚙ ⚙ ⚙ ⚙ ⚙ ⚙ ⚙

Slide 32

Slide 32 text

Modularization ● Dependency Injection using api and impl modules ● Multiple DI framework available : Dagger, Hilt, Anvil, in house… ● Better architecture, testable … faster incremental build time

Slide 33

Slide 33 text

./gradlew newModule Modularization - Tooling

Slide 34

Slide 34 text

./gradlew newModule Module type-: 1: library: 2: ui: Enter selection (default: ui:) [1-.2] Module name? (default: ): droidcon Modularization - Tooling

Slide 35

Slide 35 text

./gradlew newModule Module type-: 1: library: 2: ui: Enter selection (default: ui:) [1-.2] Module name? (default: ): droidcon BUILD SUCCESSFUL in 1m 35s Modularization - Tooling

Slide 36

Slide 36 text

How Netflix Does Builds Best Practices Eng Build mode ● Enabled by default ● Optimized for build ● No app bundle ● Less plugins Api & impl modules ● Dependency injection ● Easier testing ● Compilation avoidance ● Tooling

Slide 37

Slide 37 text

Tools to profile your build Keeping track of our build performances, measuring the impact of changes…

Slide 38

Slide 38 text

Benchmark the build ● Gradle-profiler ● Define build scenarios ● Run it ● Pro Tip: Config time analysis github.com/gradle/gradle-profiler

Slide 39

Slide 39 text

Ask the audience: Who has created a build scan?

Slide 40

Slide 40 text

Grab the lowest hanging DPE fruit: The Build Cache Faster build and test feedback cycles Improved DevProd and developer experience Higher quality software

Slide 41

Slide 41 text

Build Cache and Binary Repositories ● A build cache is very different and complementary to the concept of a binary repository (sometimes referred to as a dependency cache or artifact repository). ● Whereas a binary repository is for caching binaries that represent dependencies from one source repository to another, a build cache caches build actions, like Gradle tasks or Maven goals. A build cache makes building a single source repository faster. ● A modern and productive development environment should include both binary repositories and build caches because they are complementary. ● Because they share the goal of making the build process more efficient and because certain features overlap, it is not uncommon to assume that both address the same issues.

Slide 42

Slide 42 text

Ask the audience: do you have local build cache enabled by default?

Slide 43

Slide 43 text

How it works: Local & Remote Caching Local Cache Remote Cache Build Action (Gradle Task, Maven Goal) PUT PUT READ READ Child Node Child Node Child Node ● When the build action inputs have not changed, the output can be reused from a previous run. ● Build caching can be applied to build actions like compilation, testing, source code generation, Javadoc, checkstyle, and PMD. ● Build caching works out of the box for many standard Gradle tasks and Maven goals. ● For Maven goals or Gradle tasks that are not pre-configured to be cacheable, there is a cache configuration DSL available. That way custom build actions are also cacheable. ● The typical use case is to let CI builds push to a distributed remote build cache, and allow other CI builds as well as developer builds to pull from it. ● A local build cache is also available to reuse outputs produced earlier on the same machine.

Slide 44

Slide 44 text

Ask the audience: Who is using the remote cache?

Slide 45

Slide 45 text

How to Setup the remote build cache ● Free (via docker image) ● Gradle Enterprise ● Use build scans for analysis docs.gradle.com/build-cache-node/

Slide 46

Slide 46 text

Enabling caching of custom tasks docs.gradle.org/current/userguide/build_cache

Slide 47

Slide 47 text

When we first met “Send me a Build Scan!” scans.gradle.com

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

Build Caching: Is it enabled? How well is it working?

Slide 50

Slide 50 text

Build Scan -> Performance Tab

Slide 51

Slide 51 text

Build Scan -> Build Cache Tab

Slide 52

Slide 52 text

Build Scan -> Task Execution Tab

Slide 53

Slide 53 text

Show me all cacheable tasks that were executed Aggregate build scan data from build scans Build caching: who’s using it, who’s not? Debugging cache misses The lowest hanging fruit for speeding CI builds: Remote Build Cache

Slide 54

Slide 54 text

Build Scan -> Timeline view -> Critical Path Aggregate build scan data from build scans Build caching: who’s using it, who’s not? Debugging cache misses The lowest hanging fruit for speeding CI builds: Remote Build Cache

Slide 55

Slide 55 text

docs.gradle.org/current/userguide/build_cache_debugging.html Debugging Build Cache Misses with Task Input File Capturing

Slide 56

Slide 56 text

Build Bottlenecks discovered & Resolved Problem Solution Many cache misses locally due to empty directories remaining in source tree Gradle Doctor Plugin: Removing local directories led to more cache hits. Issues with core AGP tasks were causing cache misses Gradle Cache Fix Plugin: Applying the android-cache-fix-gradle-plugin fixed many of these issues. The Gradle team feeds these improvements back to the AGP team to integrate AGP was generating PNG files for SVG inputs, but the outputs were not reproducible resulting in downstream cache misses that were exposed by GE Since the PNG files were not required for debug, this file generation was removed. assembleDebug execution time went from 3:41 to 0:27. The generateBuildConfig task was configured with a timestamp input that resulted in `BuildConfig.java` being different on each build invocation causing caused downstream changes in compiled outputs, and meant that local developers were not able to use these from the remote cache. Moving this volatility to a leaf module resulted in cache hits for all other modules (but still got a cache miss for that particular leaf module).

Slide 57

Slide 57 text

First Results of Remote Build Cache for Netflix CI CI build/test time has improved by 46%. Average CI build time is down from 14.6 min to 8 min. The remote cache is saving 6.6 min per CI build on aggregate for all CI builds. ● 24 min of build execution time saved per CI build ● Serial Execution Factor: 3.5 ● Result: 6.6 Min wait time saved per CI build (24.5/3.5)

Slide 58

Slide 58 text

First Results of Remote Build Cache for Netflix CI The best case for CI is 3 min 18 sec!

Slide 59

Slide 59 text

Impact of remote caching on PR build times ● Average PR check build time is down from 39 min to 25 min ● Best case ~11 min 25 sec. ● The remote cache is saving on average 14 min per PR build. ● The Netflix Android team ran 251 PR check builds last week.

Slide 60

Slide 60 text

Impact of remote caching on PR Build Times Best Case is 11 min 25 sec 11 min

Slide 61

Slide 61 text

One year later…

Slide 62

Slide 62 text

Cache misses are back ● Different JDK Versions: Identify and fix major differences in JDK between CI/local that were causing cache misses on all compile jobs ● Setting Android Properties: Identify that cache misses were occurring for CompileLibraryResourcesTask which were fixed by enabling certain Android properties ● Older Versions of Dagger: Identify a cacheability issue caused by an older version of Dagger and verify that the upgraded version fixed it ● Non deterministic codegen: Identify code generations with differences between CI/local

Slide 63

Slide 63 text

Cacheable Task Executed: 835 💥

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

Java version mismatch caches/transforms-3/3c71c6377bd209bb9c86571321e581b4/transformed/output caches/transforms-3/a4bcee52a5b625e21f22eee7819e81c6/transformed/output

Slide 67

Slide 67 text

Java version mismatch : 11.0.15 vs 11.0.12 💥 caches/transforms-3/3c71c6377bd209bb9c86571321e581b4/transformed/output caches/transforms-3/a4bcee52a5b625e21f22eee7819e81c6/transformed/output

Slide 68

Slide 68 text

Use Input Normalization allprojects { normalization { runtimeClasspath { metaInf { ignoreAttribute("Implementation-Version") ignoreAttribute("Created-By") } } } }

Slide 69

Slide 69 text

Cacheable Task Executed: 24 🎉

Slide 70

Slide 70 text

After we fixed all the cache misses… PR builds at record speed: best case 5 min Screen shot 5 min

Slide 71

Slide 71 text

Pop Quiz: what changed in AGP 7.1 that contributed to this speed up?

Slide 72

Slide 72 text

Lint was made Cacheable in AGP 7.1

Slide 73

Slide 73 text

Few days after…

Slide 74

Slide 74 text

Cacheable Task Executed: 515 💥

Slide 75

Slide 75 text

__Schema.kt - Apollo GraphQL code generation 💥

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

Cacheable Task Executed: 20 🎉 AssembleDebug time back down to 2 min

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

Managing dependencies ● Dependencies centralized : Dependencies.kt in buildSrc/

Slide 80

Slide 80 text

Managing dependencies ● Dependencies centralized : Dependencies.kt in buildSrc/ ● buildSrc/ is part of build script

Slide 81

Slide 81 text

Managing dependencies ● Dependencies centralized : Dependencies.kt in buildSrc/ ● buildSrc/ is part of build script ● Changes in buildSrc/ break incremental compilation

Slide 82

Slide 82 text

Gradle Version Catalog ● All dependencies centralized in a file : gradle/libs.versions.toml

Slide 83

Slide 83 text

Gradle Version Catalog ● All dependencies centralized in a file : gradle/libs.versions.toml ● Available in Gradle 7.4

Slide 84

Slide 84 text

Gradle Version Catalog ● All dependencies centralized in a file : gradle/libs.versions.toml ● Available in Gradle 7.4 ● Not breaking incremental compilation

Slide 85

Slide 85 text

Gradle Version Catalog dependencies { implementation libs.my.dep implementation libs.bundles.androidx implementation libs.bundles.compose } my-dep = { module = "group:artifact", version = "version" }

Slide 86

Slide 86 text

buildSrc/Dependencies.kt gradle/libs.versions.toml

Slide 87

Slide 87 text

Managing dependencies ● Problems: Transitive dependencies

Slide 88

Slide 88 text

Managing dependencies ● Problems: Transitive dependencies

Slide 89

Slide 89 text

Gradle Dependency locking ● From Netflix Nebula, available in Gradle ● Lock all dependencies on a file ● Fail the build when lockfiles are not up-to-date

Slide 90

Slide 90 text

Gradle dependency locking gradle/libs.versions.toml

Slide 91

Slide 91 text

Gradle dependency locking .lockfiles/app_gradle.lockfile (generated)

Slide 92

Slide 92 text

Gradle dependency locking .lockfiles/app_gradle.lockfile (generated)

Slide 93

Slide 93 text

Gradle dependency locking configurations { compileClasspath { resolutionStrategy.activateDependencyLocking() } } gradle app:dependencies --write-locks

Slide 94

Slide 94 text

Send me a build scan for analysis… @Rooz via Gradle-Community.Slack.com

Slide 95

Slide 95 text

No content

Slide 96

Slide 96 text

Performance Best Practices ⬢ Upgrade to the latest of all tools and plugins: Gradle, AGP, Kotlin ⬢ Tools: Build Scans, Gradle Profiler ⬢ Build Essentials: Daemon, Parallel, Configuration on demand ⬢ Modularize ⬢ L1: Optimize your incremental build cache ⬢ L2: Local Cache ⬢ L3: File Watching, Configuration cache ⬢ Final Boss: Remote Cache ⬢ Enable caching, parallel, file system watching, configuration cache ⬢ Identify and fix performance issues like bottlenecks, cache misses, configuration time slow downs ⬢ Use convention plugins to organize build logic ⬢ Cache Fix plugin ⬢ Gradle Doctor Plugin

Slide 97

Slide 97 text

No content

Slide 98

Slide 98 text

We’re hiring https://jobs.netflix.com Android

Slide 99

Slide 99 text

No content

Slide 100

Slide 100 text

Thank you!