Der Gradle Build Cache speichert Task-Outputs und ermöglicht deren Wiederverwendung über Build-Grenzen hinweg. Im Gegensatz zum inkrementellen Build, der nur unveränderte Tasks skippt, stellt der Build Cache vorherige Outputs wieder her, wenn die Inputs identisch sind. Diese Funktionalität transformiert Build-Performance fundamental, da Compilation, Test-Execution und andere ressourcenintensive Tasks vollständig übersprungen werden können. Der Cache arbeitet auf Task-Ebene und berechnet für jeden Task einen Cache-Key basierend auf allen relevanten Inputs.
Die Cache-Key-Berechnung berücksichtigt Task-Inputs, Gradle-Version, Task-Implementation und relevante System-Properties. Jede Änderung an diesen Faktoren resultiert in einem neuen Cache-Key und damit einem Cache-Miss. Die Granularität der Cache-Keys ermöglicht präzise Invalidierung bei gleichzeitiger Maximierung der Cache-Hit-Rate. Task-Outputs werden als Cache-Entries gespeichert, komprimiert und mit Metadaten versehen, die Validierung und Debugging unterstützen.
Der lokale Build Cache nutzt das Dateisystem zur Speicherung von Cache-Entries. Die Standard-Location im Gradle-User-Home kann konfiguriert werden, und Size-Limits verhindern unkontrolliertes Wachstum. Der lokale Cache dient primär der Performance-Optimierung für individuelle Entwickler, bietet aber keine Team-weite Sharing-Möglichkeit. Remote Build Caches adressieren diese Limitation durch zentralisierte Cache-Storage, die zwischen Entwicklern und CI-Servern geteilt wird.
// Build Cache Konfiguration
buildCache {
local {
isEnabled = true
directory = file("${rootDir}/.gradle/build-cache")
removeUnusedEntriesAfterDays = 7
}
remote<HttpBuildCache> {
isEnabled = true
url = uri("https://cache.company.com/cache/")
isPush = System.getenv("CI") == "true" || hasProperty("cachePush")
credentials {
username = System.getenv("BUILD_CACHE_USERNAME")
password = System.getenv("BUILD_CACHE_PASSWORD")
}
// Cache-Strategie
when {
System.getenv("CI") == "true" -> {
// CI Builds: Read and Write
isPush = true
isEnabled = true
}
gradle.startParameter.isOffline -> {
// Offline Mode: Local cache only
isEnabled = false
}
else -> {
// Developer Builds: Read only
isPush = false
isEnabled = true
}
}
}
}
// Task-spezifische Cache-Konfiguration
tasks.withType<JavaCompile>().configureEach {
outputs.cacheIf { true }
// Normalisierung für bessere Cache-Hits
options.isIncremental = false // Full compilation für deterministische Outputs
options.isFork = true
options.forkOptions.executable = null // Use Gradle's Java Toolchain
}
tasks.withType<Test>().configureEach {
outputs.cacheIf {
// Cache nur stabile Tests
!name.contains("Integration") && !hasProperty("test.nocache")
}
// Input-Normalisierung
inputs.property("java.version") {
System.getProperty("java.version").substringBefore("_")
}
systemProperty("user.timezone", "UTC")
systemProperty("file.encoding", "UTF-8")
}Remote Build Caches ermöglichen Cache-Sharing über Team- und System-Grenzen. Die Implementation erfolgt typischerweise über HTTP-basierte Cache-Server wie Gradle Enterprise, Artifactory oder selbst-gehostete Lösungen. Die Cache-Distribution folgt hierarchischen Patterns: CI-Builds populieren den Cache, Entwickler konsumieren Cache-Entries. Diese Asymmetrie verhindert, dass fehlerhafte lokale Builds den gemeinsamen Cache korrumpieren.
Die Netzwerk-Optimierung ist kritisch für Remote-Cache-Performance. Cache-Entries sollten komprimiert übertragen werden, und Timeouts müssen an Netzwerk-Latenz angepasst sein. Geographic Distribution über CDNs oder regionale Cache-Server reduziert Latenz für verteilte Teams. Intelligent Retry-Logic mit Exponential Backoff handhabt temporäre Netzwerk-Probleme graceful.
// Multi-Level Cache Configuration
abstract class CacheConfiguration : BuildService<CacheConfiguration.Params> {
interface Params : BuildServiceParameters {
val environment: Property<String>
val region: Property<String>
}
fun configureCaches(settings: BuildCacheConfiguration) {
// Level 1: Local Cache
settings.local {
isEnabled = true
directory = calculateLocalCacheDir()
}
// Level 2: Regional Cache
val regionalCache = settings.remote(HttpBuildCache::class.java) {
url = uri("https://cache-${parameters.region.get()}.company.com/")
isPush = shouldPushToRegional()
isEnabled = !gradle.startParameter.isOffline
// Aggressive timeouts für Regional Cache
connectTimeout = 5000
readTimeout = 10000
}
// Level 3: Global Cache (Fallback)
if (parameters.environment.get() == "ci") {
settings.remote(HttpBuildCache::class.java) {
url = uri("https://cache-global.company.com/")
isPush = false // Never push to global
isEnabled = regionalCache?.isEnabled == false
connectTimeout = 10000
readTimeout = 30000
}
}
}
private fun calculateLocalCacheDir(): File {
return when {
System.getenv("CI") == "true" -> file("/cache/gradle-build-cache")
System.getProperty("os.name").startsWith("Windows") ->
file("C:/GradleCache/build-cache")
else -> file("${System.getProperty("user.home")}/.gradle/build-cache")
}
}
}
// Cache Warming Strategy
tasks.register("warmCache") {
description = "Pre-populate build cache with common tasks"
doLast {
val criticalTasks = listOf(
"compileJava",
"compileTestJava",
"processResources",
"jar"
)
criticalTasks.forEach { taskName ->
logger.lifecycle("Warming cache for $taskName")
// Execute task with cache population
exec {
commandLine("./gradlew", taskName, "--build-cache", "--no-daemon")
environment("GRADLE_OPTS", "-Xmx2g")
}
}
// Analyze cache effectiveness
analyzeCacheStatistics()
}
}
// Cache Maintenance
tasks.register("maintainCache") {
description = "Maintain build cache health"
doLast {
val cacheDir = file("${gradle.gradleUserHomeDir}/caches/build-cache-1")
if (cacheDir.exists()) {
val cacheSize = cacheDir.walkTopDown().map { it.length() }.sum()
val cacheAge = System.currentTimeMillis() - cacheDir.lastModified()
logger.lifecycle("Cache size: ${cacheSize / 1024 / 1024} MB")
logger.lifecycle("Cache age: ${cacheAge / 1000 / 60 / 60 / 24} days")
// Remove old entries
val cutoffTime = System.currentTimeMillis() - (7 * 24 * 60 * 60 * 1000)
var removedCount = 0
var removedSize = 0L
cacheDir.walkTopDown()
.filter { it.isFile && it.lastModified() < cutoffTime }
.forEach { file ->
removedSize += file.length()
file.delete()
removedCount++
}
logger.lifecycle("Removed $removedCount old entries (${removedSize / 1024 / 1024} MB)")
}
}
}Cache-Invalidation-Strategien balancieren Correctness mit Performance. Vollständige Cache-Clears sollten vermieden werden zugunsten selektiver Invalidation. Version-basierte Invalidation nutzt Build-Tool-Version oder Project-Version als Cache-Key-Component. Time-based Expiry entfernt alte Entries automatisch. Content-based Invalidation reagiert auf Configuration-Changes. Diese Strategien garantieren Cache-Freshness ohne übermäßige Cache-Misses.
Gradle Build Scans bieten detaillierte Performance-Analyse für
Build-Prozesse. Die Scan-Generierung erfolgt automatisch mit dem
--scan Flag oder durch Plugin-Konfiguration. Scans capturen
Timeline-Information, Task-Execution-Details, Dependency-Resolution und
Cache-Performance. Diese Daten werden zu Gradle Enterprise hochgeladen
oder lokal analysiert, wodurch Performance-Bottlenecks identifizierbar
werden.
Die Timeline-Visualisierung zeigt Task-Execution chronologisch mit Parallelität und Dependencies. Critical Path Analysis identifiziert Tasks, die Build-Duration bestimmen. Performance-Comparisons zwischen Builds zeigen Degradation oder Improvement. Diese Visualisierungen machen abstrakte Performance-Probleme konkret und actionable.
// Build Scan Configuration
plugins {
id("com.gradle.enterprise") version "3.14"
}
gradleEnterprise {
buildScan {
server = "https://scans.company.com"
publishAlways()
// Performance-relevante Tags
tag(if (System.getenv("CI") != null) "CI" else "LOCAL")
tag(System.getProperty("os.name"))
tag("branch-${getCurrentBranch()}")
// Custom Values für Performance-Analyse
value("MaxHeap", Runtime.getRuntime().maxMemory().toString())
value("AvailableProcessors", Runtime.getRuntime().availableProcessors().toString())
value("CacheHitRate", calculateCacheHitRate())
// Build-Vergleiche
if (System.getenv("BASELINE_BUILD_SCAN") != null) {
link("Baseline", System.getenv("BASELINE_BUILD_SCAN"))
}
// Performance-Metriken erfassen
buildFinished {
value("TotalBuildTime", "${buildDuration}ms")
value("ConfigurationTime", "${configurationDuration}ms")
value("TaskExecutionTime", "${taskExecutionDuration}ms")
// Task-spezifische Metriken
tasks.withType<JavaCompile>().forEach { task ->
value("Compile-${task.name}", "${task.duration}ms")
}
tasks.withType<Test>().forEach { task ->
value("Test-${task.name}", "${task.duration}ms")
value("Test-${task.name}-Count", task.testCount.toString())
}
}
}
}
// Performance Profiling Task
tasks.register("profileBuild") {
description = "Profile build performance"
doLast {
val profileReport = file("${buildDir}/reports/build-profile.html")
// Execute build with profiling
exec {
commandLine(
"./gradlew", "clean", "build",
"--profile",
"--scan",
"--build-cache",
"--parallel",
"--max-workers=4"
)
}
// Analyze profile results
val profileData = file("${buildDir}/reports/profile/profile-*.html")
.listFiles()
?.maxByOrNull { it.lastModified() }
?.readText()
if (profileData != null) {
val analysis = analyzeProfileData(profileData)
profileReport.parentFile.mkdirs()
profileReport.writeText(generateProfileReport(analysis))
logger.lifecycle("Performance Profile:")
logger.lifecycle(" Total time: ${analysis.totalTime}ms")
logger.lifecycle(" Configuration: ${analysis.configurationTime}ms")
logger.lifecycle(" Task execution: ${analysis.taskExecutionTime}ms")
logger.lifecycle(" Slowest tasks:")
analysis.slowestTasks.forEach { task ->
logger.lifecycle(" ${task.name}: ${task.duration}ms")
}
}
}
}Performance-Benchmarking etabliert Baselines und trackt Trends. Regular Benchmark-Runs mit konsistenter Hardware und Configuration messen Build-Performance objektiv. Statistical Analysis über mehrere Runs eliminiert Outliers. Regression-Detection alarmiert bei Performance-Degradation. Diese systematische Messung macht Performance zur messbaren Metrik statt subjektiver Wahrnehmung.
Task-Parallelisierung nutzt Multi-Core-Prozessoren für simultane
Task-Execution. Gradle’s Dependency-Graph bestimmt, welche Tasks
parallel laufen können. Die --parallel Flag aktiviert
Project-Level-Parallelität in Multi-Project-Builds. Die
org.gradle.parallel Property macht Parallelisierung zum
Default. Worker-Count-Configuration balanciert Parallelität mit
Memory-Constraints.
Die Worker API ermöglicht Intra-Task-Parallelität für CPU-intensive Operations. Custom Tasks können Work-Items parallel über Worker-Threads verarbeiten. Die API bietet Isolation-Modes von Shared-Classloader bis Process-Isolation. Diese Granularität ermöglicht sichere Parallelisierung auch bei State-behafteten Operations.
// Worker API Implementation
abstract class ParallelProcessingTask : DefaultTask() {
@get:InputFiles
abstract val inputFiles: ConfigurableFileCollection
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Inject
abstract val workerExecutor: WorkerExecutor
@TaskAction
fun process() {
val workQueue = workerExecutor.processIsolation {
forkOptions {
maxHeapSize = "512m"
systemProperty("file.encoding", "UTF-8")
}
}
inputFiles.files.forEach { inputFile ->
workQueue.submit(ProcessingWorkAction::class.java) {
inputFile.set(inputFile)
outputFile.set(outputDir.file("processed/${inputFile.name}"))
}
}
// Wait for all work to complete
workQueue.await()
}
}
abstract class ProcessingWorkAction : WorkAction<ProcessingWorkParameters> {
override fun execute() {
val input = parameters.inputFile.get().asFile
val output = parameters.outputFile.get().asFile
// CPU-intensive processing
processFile(input, output)
}
}
interface ProcessingWorkParameters : WorkParameters {
val inputFile: Property<File>
val outputFile: Property<File>
}
// Parallelization Configuration
tasks.withType<JavaCompile>().configureEach {
options.isFork = true
options.forkOptions.memoryInitialSize = "256m"
options.forkOptions.memoryMaximumSize = "1g"
}
tasks.withType<Test>().configureEach {
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
// Memory pro Fork
minHeapSize = "256m"
maxHeapSize = "512m"
// JVM-Reuse zur Overhead-Reduktion
forkEvery = 100
}
// Parallel Execution Control
gradle.taskGraph.whenReady {
val maxWorkers = when {
System.getenv("CI") == "true" -> 8
gradle.startParameter.isOffline -> 2
else -> 4
}
gradle.startParameter.maxWorkerCount = maxWorkers
// Adjust based on available memory
val availableMemory = Runtime.getRuntime().maxMemory()
val memoryPerWorker = availableMemory / maxWorkers
if (memoryPerWorker < 512 * 1024 * 1024) {
logger.warn("Low memory per worker: ${memoryPerWorker / 1024 / 1024}MB")
gradle.startParameter.maxWorkerCount = (availableMemory / (512 * 1024 * 1024)).toInt()
}
}Task-Avoidance-API reduziert Configuration-Time durch Lazy Task-Creation. Tasks werden nur konfiguriert, wenn sie tatsächlich ausgeführt werden. Register-statt-Create Patterns verschieben Task-Instantiation. Configuration-Cache persistiert Configuration-State über Builds. Diese Optimierungen reduzieren Overhead besonders in großen Projects mit vielen Tasks.
Dependency-Resolution konsumiert signifikante Build-Zeit, besonders bei ersten Builds oder Dependency-Updates. Resolution-Caching speichert Dependency-Metadata lokal und vermeidet Repository-Requests. Dynamic-Version-Caching mit TTLs balanciert Freshness mit Performance. Repository-Ordering priorisiert schnelle oder lokale Repositories. Diese Optimierungen reduzieren Network-Latency und Resolution-Time.
Dependency-Locking fixiert Dependency-Versionen für reproduzierbare Builds. Lock-Files dokumentieren resolved Versions und vermeiden repeated Resolution. Selective Locking ermöglicht controlled Updates einzelner Dependencies. Platform-BOMs und Version-Catalogs zentralisieren Version-Management und reduzieren Resolution-Complexity.
// Dependency Resolution Optimization
configurations.all {
resolutionStrategy {
// Cache dynamic versions
cacheDynamicVersionsFor(10, TimeUnit.MINUTES)
cacheChangingModulesFor(4, TimeUnit.HOURS)
// Fail fast on conflicts
failOnVersionConflict()
// Force specific versions for consistency
eachDependency {
if (requested.group == "org.slf4j") {
useVersion("2.0.9")
because("Standardize logging framework version")
}
}
// Component selection rules
componentSelection {
all {
if (candidate.module == "commons-collections" &&
candidate.version.matches(Regex("3\\.[0-2]\\.[0-9]"))) {
reject("Security vulnerability in commons-collections < 3.2.2")
}
}
}
}
}
// Repository Optimization
repositories {
// Local/Fast repositories first
mavenLocal {
content {
includeGroup("com.company.internal")
}
}
// Corporate repository with content filtering
maven {
url = uri("https://nexus.company.com/repository/maven-public")
content {
includeGroupByRegex("com\\.company\\..*")
excludeGroup("com.external")
}
metadataSources {
mavenPom()
artifact() // Avoid expensive metadata when not needed
}
}
// Public repositories last
mavenCentral {
content {
excludeGroupByRegex("com\\.company\\..*")
}
}
// Repository timeout configuration
all {
if (this is MavenArtifactRepository) {
authentication {
create<BasicAuthentication>("basic")
}
// Connection pooling
System.setProperty("http.maxConnections", "10")
System.setProperty("http.connectionTimeout", "5000")
}
}
}
// Dependency Locking
dependencyLocking {
lockAllConfigurations()
lockMode.set(LockMode.STRICT)
}
tasks.register("updateDependencyLocks") {
description = "Update dependency lock files"
doFirst {
configurations.all {
resolutionStrategy.deactivateDependencyLocking()
}
}
dependsOn(configurations.compileClasspath.name)
dependsOn(configurations.runtimeClasspath.name)
dependsOn(configurations.testCompileClasspath.name)
}
// Dependency Analysis
tasks.register("analyzeDependencyResolution") {
description = "Analyze dependency resolution performance"
doLast {
val report = file("${buildDir}/reports/dependency-analysis.txt")
report.parentFile.mkdirs()
report.writeText(buildString {
appendLine("Dependency Resolution Analysis")
appendLine("==============================")
configurations.filter { it.isCanBeResolved }.forEach { config ->
val start = System.currentTimeMillis()
val resolved = config.resolvedConfiguration.lenientConfiguration
val duration = System.currentTimeMillis() - start
appendLine("\nConfiguration: ${config.name}")
appendLine(" Resolution time: ${duration}ms")
appendLine(" Total dependencies: ${resolved.allModuleDependencies.size}")
appendLine(" Direct dependencies: ${config.dependencies.size}")
// Find slowest resolving dependencies
val slowDeps = measureDependencyResolution(config)
if (slowDeps.isNotEmpty()) {
appendLine(" Slowest dependencies:")
slowDeps.take(5).forEach { (dep, time) ->
appendLine(" $dep: ${time}ms")
}
}
}
})
logger.lifecycle("Dependency analysis written to ${report.absolutePath}")
}
}Composite Builds optimieren Multi-Repository-Development durch Source-Dependencies. Statt Binary-Dependencies aus Repositories werden lokale Projects direkt eingebunden. Diese Substitution eliminiert Publishing-Overhead während Development. Automatic Classpath-Substitution macht Integration transparent. Build-Cache-Sharing zwischen Composite-Participants maximiert Cache-Reuse. Diese Technik beschleunigt iterative Development über Repository-Grenzen erheblich.