29 Build Cache und Performanceoptimierung

29.1 Build-Cache-Architektur und Funktionsweise

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")
}

29.2 Remote-Cache-Strategien

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.

29.3 Performance-Analyse mit Build Scans

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.

29.4 Task-Parallelisierung und Worker API

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.

29.5 Dependency-Resolution-Optimierung

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.