How We Slashed Android Gradle Sync Time from 8 Minutes to 1.5 Minutes
Facing 8‑minute Gradle syncs, frequent GC‑over‑limit errors, and excessive memory usage, the team diagnosed severe memory leaks in Android build configurations, applied variantFilter filtering, tuned JVM arguments, and optimized daemon settings, ultimately reducing sync time to 1.5 minutes and cutting CI build time in half.
Background
As the "big‑learning‑lamp" business grew, the Dalil client’s compilation degraded dramatically: a full project sync took up to five minutes, local builds were extremely slow, and GC‑over‑limit errors frequently occurred, severely hurting developer productivity. CI builds often exceeded 20 minutes, impacting merge efficiency.
Initial Investigation
First, we measured local compilation:
Full initial compilation: ~10 minutes.
First incremental build (no changes): ~15 minutes.
Second incremental build: GC‑over‑limit error.
Each build left a Java process using the full 8 GB heap. Syncing the project locally took ~9 minutes the first time and ~10 minutes the second time, then failed with the same GC error. The heavy memory usage also caused noticeable heating and sluggishness on developers’ machines.
Analysis Approach
Observing that memory consumption multiplied after each sync, we identified a serious memory‑leak problem in the Gradle daemon. Fixing the leak would directly improve compilation speed.
Memory Leak Mitigation – Variant Filtering
We focused on the sync scenario because it reproduces the leak more simply than a full build. Using VisualVM we monitored the Gradle daemon’s heap during sync and saw it fill to 8 GB, with an actual RSS of 6.3 GB. A heap dump analyzed with Eclipse MAT revealed that DefaultConfiguration_Decorated consumed 83% of the memory. The project contains three product flavors, >80 modules, and two build types (debug/release). During sync, Gradle loads configurations for every flavor‑build‑type combination, inflating memory usage to ~5 GB. To reduce this, we applied the variantFilter API from the Android Gradle plugin to hide unnecessary flavors during development:
if (!project.rootProject.ext.BuildContext.isCI) {
// Reduce configuration count for local development
afterEvaluate {
if (it.plugins.hasPlugin('com.android.library') || it.plugins.hasPlugin('com.android.application')) {
def flavorName = DEFAULT_FLAVOR_NAME
def mBuildType = DEFAULT_BUILD_TYPE
boolean needIgnore = false
for (String s : gradle.startParameter.taskNames) {
s = s.toLowerCase()
println("variantFilter taskName = ${s}")
if (s.contains("publish") || s.contains("checkchanged")) {
needIgnore = false
}
if (s.contains("release")) {
mBuildType = "release"
}
if (s.contains("flavor1")) {
flavorName = "flavor1"
break
} else if (s.contains("flavor2")) {
flavorName = "flavor2"
break
} else if (s.contains("flavor3")) {
flavorName = "flavor3"
break
}
}
if (needIgnore) {
println("variantFilter flavorName = ${flavorName}, mBuildType = ${mBuildType}")
android {
variantFilter { variant ->
def names = variant.flavors*.name
if (!names.empty && !names.contains(flavorName)) {
setIgnore(true)
println("ignore variant ${names}")
}
def buildType = variant.getBuildType().getName()
if (buildType != mBuildType) {
setIgnore(true)
println("ignore variant ${buildType}")
}
}
}
}
}
}
}We also added default flavor and build‑type settings to gradle.properties :
# flavor default setting
DEFAULT_FLAVOR_NAME = flavor1
DEFAULT_BUILD_TYPE = debugAfter applying the filter, sync memory dropped to a 5.5 GB heap with only 3.2 GB RSS, saving roughly 3 GB of RAM.
Further Leak Investigation
Repeated syncs still caused memory to double, suggesting additional leaks. Heap dumps showed that configuration objects doubled after each sync. Using MAT’s “Leak Suspects” we identified two suspicious roots: ActionRunningListener Configuration objects The first leak traced to a VisitableURLClassLoader held by a thread from the Build Scan plugin. The second, larger leak (≈1.1 GB) originated from a third‑party plugin’s KVJsonHelper class, which kept a static reference to the Gradle object. Because Gradle creates a new daemon process for each sync, each new daemon introduced a fresh classloader, preventing the old daemon from being garbage‑collected. After coordinating with the plugin owners to fix the static reference, the leaks were eliminated, reducing overall memory consumption from 3 GB to about 100 MB. Gradle JVM Tuning Even after fixing leaks, build times remained high: CI release builds took >20 minutes, with the R8 task alone consuming 18 minutes and GC accounting for ~12 minutes. The JVM heap (8 GB) was insufficient, so we increased it to 16 GB: <code>org.gradle.jvmargs=-Xmx16384M -XX:MaxPermSize=8192m -Dkotlin.daemon.jvm.options="-Xmx8192M" </code> This change cut CI release build time from 20 minutes to 10 minutes, mainly by reducing GC overhead from >50% to ~5%. We also experimented with -XX:MaxHeapFreeRatio=60 -XX:MinHeapFreeRatio=40 to control free‑heap proportion, but observed that the JVM does not shrink the heap in real time, so the parameters had limited effect. Finally, we shortened the Gradle daemon’s idle timeout from the default three hours to one hour to prevent long‑running daemons from hogging memory when idle. Results Memory leaks were eliminated; repeated builds now show only a gradual memory increase. Sync time dropped from ~8 minutes to 1.5 minutes, dramatically improving local development speed. CI build time halved from 20 minutes to 10 minutes, with GC time reduced from half the build duration to under 5%. Key Takeaways In multi‑flavor Android projects, use variantFilter to exclude unnecessary variants and cut configuration memory usage. Avoid static references to the Gradle object inside custom plugins to prevent daemon‑level memory leaks. Configure Gradle daemon JVM parameters (heap size, GC ratios, idle timeout) to keep GC overhead low and ensure stable build performance.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ByteDance Dali Intelligent Technology Team
Technical practice sharing from the ByteDance Dali Intelligent Technology Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
