Ammunition in the war against Teams Android build time

Posted by

This post has been republished via RSS; it originally appeared at: Microsoft Mobile Engineering - Medium.

The mobile industry is an ever-evolving space, with new innovations and updates being introduced regularly. This means that the competition is fierce, and every day counts in shipping features and bug fixes. The time it takes to build an Android app is a crucial factor that impacts developer productivity, which in turn can affect the app’s success. The percentage reduction in build time leads to percentage increase in shipping features and bug fixes. At Microsoft Teams Android, we understand this well and have been working relentlessly to improve our Android build time over the past quarter.

Photo by Venti Views on Unsplash

Our Android codebase comprises of 300+ modules that are contributed by hundreds of developers who run thousands of builds per day, both locally and in our Continuous Integration (CI) and Pull Request (PR) triggered pipelines.

1. Build Telemetry

We aimed to start by setting a baseline for our build configuration and execution time on both the project and task levels. To do so, we collected telemetry from all developers in different environments and machines.

We started with setting up build telemetry for each gradle task. For this we used TaskExecutionAdapter and BuildListener that sent build telemetry to our monitoring servers by listening via the Gradle.addListener API. Along with the gradle task execution time, we also sent device and development details, branch creation dates, cores, RAM, OS, and the list of modules changed.

Shortened output format,

{‘Build_Time’: 82, ‘deviceId’: ‘<>’, ‘modelName’: ‘MacBook Pro’, ‘modelIdentifier’: ‘MacBookPro18,2’, ‘processorName’: ‘Apple M1 Max’, ‘totalCores’: ’10 (8 performance and 2 efficiency)’, ‘memory’: ’32 GB’, ‘timezone’: ‘+0530’, ‘task’: ‘preparekotlinbuildscriptmodel’, ‘taskData’: ‘{}’, ‘os’: ‘Darwin’, ‘cleanBuild’: False, ‘totalTaskTime’: 0, ‘versionName’: ‘1416/’, ‘modulesChanged’: ‘[“apps/teams”, “common/beacon”]’}

This helped us collect data and build monitoring on a daily basis to validate regressions and improvements.

2. Gradle Upgrade

Gradle, the build system Teams Android codebase uses, comes with upgrades on a quarterly if not monthly basis. We upgraded to the latest Gradle version at regular intervals, going from 5.6 to 6.5 to 7.4 and now working on getting to 8.0. Every Gradle version comes with features, some of which are experimental, and multiple build performance improvements. Upgrading Gradle to the latest version led to a 30–40% improvement in build time.

Note that each upgrade comes with Gradle API deprecations, new lint rules needing disable or baselines, other build tool changes, library upgrades needed, and many other changes that are generally not anticipated thoroughly before diving in. For eg: Upgrade from 5 to 6 led to 1000s of lint breakages across the codebase due to new lint checks added every gradle update which had to be disabled and many plugin upgrades on older APIs.

3. Compiler flags and conditional build process

By default Android Studio enables all the steps in our build process for debug builds. We switched off multiple steps to speed up our dev build time using compiler flags such as below,

def isRelease = gradle.startParameter.taskNames.contains("Release")
def disableABISplit = project.hasProperty("disableABISplit")
def disableResConfigs = project.hasProperty("disableResConfigs")
def disableCruncher = project.hasProperty("disableCruncher")
def enableMinify = project.hasProperty("enableMinify")

Passing them through compile options cutting out minutes of build time by disabling minification, resource crunching, etc.

We also disabled multiple build features from our as the codebase doesn’t need them,

# Disable Build Features in modules by default unless enabled

4. Non-transitive R

There are multiple blogs about enabling android.nonTransitiveRClass This strips resource IDs from transitive R files as from below diagram warranting fully qualifying imports at all places.

This was a significant effort we took as part of a hackathon. We had thousands of files depending on this repeated R values. So we went with a pre-commit approach, by qualifying imports using the tool from Android Studio and then manually over few weeks. After 1600 file changes across 100s of modules we enabled the flag to work seamlessly.

Non-transitive R has improved our apk size by 2.6 MB by removing close to 400000+ resource IDs. This also breaks a lot of dependency among modules, improving our incremental build time by 13%.

5. Gradle Lazy APIs

Many of the scripts were using eager APIs from Gradle which ended up creating and registering a lot of tasks on configuration time. Many tasks may be used by only certain build variants or not needed to be configured for developer builds.

We mapped and replaced all APIs to lazy configure for Task avoidance and validated the avoidance with Gradle scans. You can follow the below link to map all your usages to lazy APIs across all your gradle build scripts,

6. Task input normalisation

We observed on our gradle scans that many gradle tasks were not cached and realised we were passing timestamp with the task inputs inside Adding normalisation ignores the file for cache keys in Gradle.

Check your Gradle scans and ideally most tasks should come from cache on incremental runs, diagnose them if they don’t.

normalization {
runtimeClasspath {
ignore ''

More reference below:

Authoring Tasks

7. Annotation processing optimisation

Teams extensively uses Dagger across most modules for injection and we also have our own annotation processors for various internal code plus some other libraries using kapt.

We noticed kapt plugin being applied to multiple scripts even if the module was not using annotation processor so we cleaned that up. Apply annotation processor plugin only if necessary. Kapt verbose helps you in spotting why kapt is being run and which libraries are taking your most time.

With ksp on the block, kapt is on the sunset but till then the below should move the needle,

# KAPT properties
# Enables verbose printer when kapt runs

# Incremental annotation processing

# Includes annotation discovery on compile classpath vs runtime

kapt.classloaders.cache.size=400 # > Number of modules roughly

8. Compiler improvements

Compilation is central part of the developer’s changes. The fastest way to compile is not to compile at all. We achieved massive compilation avoidance by our modularisation efforts and breaking dependency between modules.

Speeding up the compiler was the next step. We migrated java files to kotlin in some critical modules as mixed files lead to compiler taking a longer time and more memory. We also used the below flags to speed things up,

# File system watching by Gradle,

# We use parallel GC for higher throughput and work with 10 GB heapspace for developer builds
org.gradle.jvmargs=-XX:+UseParallelGC -Xss4m -Xms2g -Xmx10g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:HeapDumpPath=/reports/heapDumps

# Enables databinding annotation processors to work incrementally

kotlin.incremental.usePreciseJavaTracking=true #

The other compilation improvements in the pipeline is using the K2 Compiler in Kotlin 2.0, migrating more modules to kotlin-only.

9. Configuration Time

Improving configuration time is critical as configuration is before every gradle task execution. We enabled configureOnDemand which calculates the task graph and configures only the tasks relevant for the Gradle task to be run instead of configuring all modules.

# Configures only modules that are needed for the executed task

Configuration cache is under review due to incompatibility with certain plugins but once in, can give us around 20% improvement for our Gradle task runs. #5, #6, #7, #8, #9 have given us 10–15% improvements.

10. Hardware

One of the biggest gains we got in our build time and developer productivity was by moving to M1/M2 chips from Apple from the Intel based macbook machines. We noticed around 50% improvements in our builds from just switching to M1 Max Macbook pro machines.

New hardware is a big investment ask from the finance team and always needs a strong business case to make it worthwhile. Making a business case by mapping decrease in build time per build in median and 95th percentile to saved developer hours per month effectively attaches a $ value to it.

Dev hours = est time saved in minutes per build /60 * avg no of builds/day/dev

For eg for us,

6 minutes/60* 20 builds/day/dev = 2 dev hours per day

This means each dev can either ship 20% more from the product backlog, or 20% more devs can be hired or 20% cost can be saved. All three angles can be win-win-win. :)


All the above efforts meant consistent attacks at build time from whatever we could find on the internet. After going over some excellent talks, blogs, scouring Gradle docs, Google Developer videos and lots of reasoning over 10s if not 100s of gradle build scans we have dropped our build time from 9–10 minutes to under 5 minutes on average.

Ammunition in the war against Teams Android build time was originally published in Microsoft Mobile Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.