要充分利用任務輸出快取,重要的是正確指定任務的任何必要輸入,同時避免不必要的輸入。未指定影響任務輸出的輸入可能會導致組建不正確,而無謂地指定不影響任務輸出的輸入可能會導致快取未命中。

本章節說明如何找出快取未命中的原因。如果您遇到預期之外的快取命中,我們建議將您預期會觸發快取未命中的任何變更宣告為任務的輸入。

找出任務輸出快取的問題

以下我們說明一個逐步的流程,應該有助於解決組建中快取的任何問題。

確保增量組建運作

首先,確保您的組建在沒有快取的情況下執行正確的動作。執行組建兩次,不啟用 Gradle 組建快取。預期的結果是所有產生檔案輸出的可執行任務都是最新的。您應該在命令列上看到類似以下的內容

$ ./gradlew clean --quiet (1)
$ ./gradlew assemble (2)

BUILD SUCCESSFUL
4 actionable tasks: 4 executed

$ ./gradlew assemble (3)

BUILD SUCCESSFUL
4 actionable tasks: 4 up-to-date
1 先執行 clean,確保我們從沒有任何剩餘結果開始。
2 我們假設您的組建在這些範例中透過執行 assemble 任務表示,但您可以替換任何對您的組建有意義的任務。
3 再次執行組建,不執行 clean
沒有輸出或輸入的任務將會一直執行,但這不應該是問題。

使用以下所述的方法來診斷修正應該是最新的,但卻不是最新的任務。如果您發現某個任務已過時,但沒有可快取的任務取決於其結果,那麼您不必對此做任何事。目標是為可快取的任務達成穩定的任務輸入

使用本機快取進行就地快取

當您滿意最新的效能時,您可以重複上述實驗,但這次使用乾淨的建置,並開啟建置快取。使用乾淨的建置並開啟建置快取的目標是從快取中擷取所有可快取的任務。

執行此測試時,請確定您未設定任何遠端快取,且已啟用儲存在本機快取中。這些是預設設定。

在命令列上,這看起來會像這樣

$ rm -rf ~/.gradle/caches/build-cache-1 (1)
$ ./gradlew clean --quiet (2)
$ ./gradlew assemble --build-cache (3)

BUILD SUCCESSFUL
4 actionable tasks: 4 executed

$ ./gradlew clean --quiet (4)
$ ./gradlew assemble --build-cache (5)

BUILD SUCCESSFUL
4 actionable tasks: 1 executed, 3 from cache
1 我們想從空的本機快取開始。
2 清除專案,移除之前建置中任何不需要的殘留檔案。
3 建置一次,讓它填滿快取。
4 再次清除專案。
5 再次建置:這次所有可快取的內容都應該從剛才填滿的快取中載入。

您應該會看到所有可快取的任務都從快取載入,而不可快取的任務應該會執行。

fully cached task execution

再次使用以下方法來診斷修正快取能力問題。

測試快取可重新定位性

當在啟用本機快取的情況下建置相同的簽出時,所有內容都正確載入後,現在該看看是否有任何重新定位問題。如果任務的輸出可以在不同位置執行任務時重複使用,則該任務被視為可重新定位。(在路徑敏感性和可重新定位性中進一步說明。)

應該可重新定位但卻不可重新定位的任務通常是因為任務的輸入中存在絕對路徑。

若要找出這些問題,請先在電腦上的兩個不同目錄中簽出專案的相同提交。對於以下範例,我們假設我們在\~/checkout-1\~/checkout-2中簽出。

與前一個測試一樣,您不應該設定任何遠端快取,且應該啟用儲存在本機快取中。
$ rm -rf ~/.gradle/caches/build-cache-1 (1)
$ cd ~/checkout-1 (2)
$ ./gradlew clean --quiet (3)
$ ./gradlew assemble --build-cache (4)

BUILD SUCCESSFUL
4 actionable tasks: 4 executed

$ cd ~/checkout-2 (5)
$ ./gradlew clean --quiet (6)
$ ./gradlew clean assemble --build-cache (7)

BUILD SUCCESSFUL
4 actionable tasks: 1 executed, 3 from cache
1 首先移除本機快取中的所有項目。
2 前往第一個簽出目錄。
3 清除專案,移除之前建置中任何不需要的殘留檔案。
4 執行建置以填入快取。
5 移至其他結帳目錄。
6 再次清除專案。
7 再次執行建置。

您應該會看到與先前就地快取測試步驟中完全相同的結果。

跨平台測試

如果您的建置通過搬移測試,表示建置狀態良好。如果您的建置需要支援多個平台,最好先確認所需的任務是否也會在平台之間重複使用。跨平台建置的典型範例是,CI 在 Linux VM 上執行,而開發人員使用 macOS 或 Windows,或不同種類或版本的 Linux。

若要測試跨平台快取重複使用,請設定一個遠端快取(請參閱在 CI 建置之間分享結果),並從一個平台填入快取,再從另一個平台使用快取。

增量快取使用

在對完全快取的建置進行這些實驗後,您可以繼續嘗試對專案進行一般變更,並查看是否仍快取足夠的任務。如果結果不令人滿意,您可以考慮重新調整專案結構,以減少不同任務之間的相依性。

評估快取效能(隨著時間推移)

考慮記錄建置執行時間、產生圖表,並分析結果。注意某些模式,例如建置重新編譯所有內容,即使您預期編譯會快取。

您也可以手動或自動變更程式碼庫,並檢查是否快取預期的任務組。

如果您有任務重新執行,而不是從快取載入其輸出,則這可能會指出建置中的問題。下一個區段說明了除錯快取未命中的技巧。

有助於診斷快取未命中的資料

當 Gradle 為任務計算建置快取金鑰,而該金鑰與快取中現有的任何建置快取金鑰不同時,就會發生快取未命中。只比較建置快取金鑰本身並無法提供太多資訊,因此我們需要查看一些更細微的資料,才能診斷快取未命中。您可以在快取任務區段中找到計算建置快取金鑰的所有輸入清單。

從最粗略到最細微,我們將用於比較兩個任務的項目為

  • 建立快取金鑰

  • 任務和任務動作實作

    • 類別載入器雜湊

    • 類別名稱

  • 任務輸出屬性名稱

  • 個別任務屬性輸入雜湊

  • 屬於任務輸入屬性的檔案雜湊

如果您想要有關建置快取金鑰和個別輸入屬性雜湊的資訊,請使用 -Dorg.gradle.caching.debug=true

$ ./gradlew :compileJava --build-cache -Dorg.gradle.caching.debug=true

.
.
.
Appending implementation to build cache key: org.gradle.api.tasks.compile.JavaCompile_Decorated@470c67ec713775576db4e818e7a4c75d
Appending additional implementation to build cache key: org.gradle.api.tasks.compile.JavaCompile_Decorated@470c67ec713775576db4e818e7a4c75d
Appending input value fingerprint for 'options' to build cache key: e4eaee32137a6a587e57eea660d7f85d
Appending input value fingerprint for 'options.compilerArgs' to build cache key: 8222d82255460164427051d7537fa305
Appending input value fingerprint for 'options.debug' to build cache key: f6d7ed39fe24031e22d54f3fe65b901c
Appending input value fingerprint for 'options.debugOptions' to build cache key: a91a8430ae47b11a17f6318b53f5ce9c
Appending input value fingerprint for 'options.debugOptions.debugLevel' to build cache key: f6bd6b3389b872033d462029172c8612
Appending input value fingerprint for 'options.encoding' to build cache key: f6bd6b3389b872033d462029172c8612
.
.
.
Appending input file fingerprints for 'options.sourcepath' to build cache key: 5fd1e7396e8de4cb5c23dc6aadd7787a - RELATIVE_PATH{EMPTY}
Appending input file fingerprints for 'stableSources' to build cache key: f305ada95aeae858c233f46fc1ec4d01 - RELATIVE_PATH{.../src/main/java=IGNORED / DIR, .../src/main/java/Hello.java='Hello.java' / 9c306ba203d618dfbe1be83354ec211d}
Appending output property name to build cache key: destinationDir
Appending output property name to build cache key: options.annotationProcessorGeneratedSourcesDirectory
Build cache key for task ':compileJava' is 8ebf682168823f662b9be34d27afdf77

例如,記錄檔會顯示哪些來源檔案構成 compileJava 任務的 stableSources。若要找出兩次建置之間的實際差異,您需要自行比對和比較這些雜湊。

Develocity 已為您處理這項工作;它讓您可以使用 Build Scan™ 比較工具快速診斷快取遺失。

診斷快取遺失的原因

準備好上一節的資料後,您應該能夠診斷出為什麼找不到特定任務的輸出。由於您預期會快取更多任務,因此您應該能夠精確找出會產生有問題成品的建置。

在深入探討如何找出為何無法從快取載入一個任務之前,我們應該先找出導致快取遺失的任務。有一種連鎖效應,如果建置中較早的任務無法從快取載入且輸出不同,就會導致依賴任務執行。因此,您應該找出已執行的第一個可快取任務,然後從那裡繼續調查。這可以在 Build Scan™ 的時間軸檢視中完成

first non cached task

首先,您應該檢查任務的實作是否已變更。這表示要檢查任務類別本身和每個動作的類別名稱和類別載入器雜湊。如果有變更,這表示建置指令碼、buildSrc 或 Gradle 版本已變更。

buildSrc 輸出的變更也會標記為已變更,您建置新增的所有邏輯。特別是,新增到可快取任務的客製化動作會標記為已變更。這可能會造成問題,請參閱 關於 doFirstdoLast 的區段

如果實作相同,則您需要開始比較兩個建置之間的輸入。應該至少有一個不同的輸入雜湊。如果是簡單的值屬性,則任務的組態已變更。例如,透過下列方式發生:

  • 變更建置指令碼,

  • 針對 CI 或開發人員建置有條件地變更任務組態,

  • 依賴於任務組態的系統屬性或環境變數,

  • 或具有作為輸入一部分的絕對路徑。

如果變更的屬性是檔案屬性,則原因可能與值屬性的變更相同。不過,最有可能的是檔案系統上的檔案以 Gradle 偵測到此輸入有差異的方式變更。最常見的情況是原始碼已透過簽入變更。也有可能由任務產生的檔案已變更,例如,因為它包含時間戳記。如 Java 版本追蹤 所述,Java 版本也會影響 Java 編譯器的輸出。如果您不希望檔案成為任務的輸入,則有可能您應該變更任務的組態以不包含它。例如,讓您的整合測試組態包含所有單元測試類別作為相依項,其效果是當單元測試變更時,所有整合測試都會重新執行。另一個選項是任務追蹤絕對路徑,而不是相對路徑,且專案目錄的位置已在磁碟上變更。

範例

我們將引導您完成診斷快取遺漏的流程。假設我們建置 A 和建置 B,並且我們預期子專案 sub1 的所有測試任務都應該快取在建置 B 中,因為只有另一個子專案 sub2 的單元測試已變更。但相反地,該子專案的所有測試都已執行。由於在快取遺漏時我們有連鎖效應,因此我們需要找出導致快取鏈失敗的任務。這可以透過篩選所有已執行的可快取任務,然後選取第一個任務來輕鬆完成。在我們的案例中,發現子專案 internal-testing 的測試已執行,即使此專案沒有程式碼變更。這表示屬性 classpath 已變更,且執行時期類別路徑上的某些檔案實際上已變更。深入探究後,我們實際上看到該專案中 processResources 任務的輸入也已變更。最後,我們在建置檔案中找到以下內容

build.gradle.kts
val currentVersionInfo = tasks.register<CurrentVersionInfo>("currentVersionInfo") {
    version = project.version as String
    versionInfoFile = layout.buildDirectory.file("generated-resources/currentVersion.properties")
}

sourceSets.main.get().output.dir(currentVersionInfo.map { it.versionInfoFile.get().asFile.parentFile })

abstract class CurrentVersionInfo : DefaultTask() {
    @get:Input
    abstract val version: Property<String>

    @get:OutputFile
    abstract val versionInfoFile: RegularFileProperty

    @TaskAction
    fun writeVersionInfo() {
        val properties = Properties()
        properties.setProperty("latestMilestone", version.get())
        versionInfoFile.get().asFile.outputStream().use { out ->
            properties.store(out, null)
        }
    }
}
build.gradle
def currentVersionInfo = tasks.register('currentVersionInfo', CurrentVersionInfo) {
    version = project.version
    versionInfoFile = layout.buildDirectory.file('generated-resources/currentVersion.properties')
}

sourceSets.main.output.dir(currentVersionInfo.map { it.versionInfoFile.get().asFile.parentFile })

abstract class CurrentVersionInfo extends DefaultTask {
    @Input
    abstract Property<String> getVersion()

    @OutputFile
    abstract RegularFileProperty getVersionInfoFile()

    @TaskAction
    void writeVersionInfo() {
        def properties = new Properties()
        properties.setProperty('latestMilestone', version.get())
        versionInfoFile.get().asFile.withOutputStream { out ->
            properties.store(out, null)
        }
    }
}

由於 Java 的 Properties.store 方法所儲存的屬性檔案包含時間戳記,因此這將導致每次建置執行時執行時期類別路徑都會變更。若要解決此問題,請參閱 不可重複的任務輸出 或使用 輸入正規化

編譯類別路徑不受影響,因為編譯避免會忽略類別路徑上的非類別檔案。