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

本章是關於找出快取未命中發生的原因。如果您有意外的快取命中,我們建議將您期望觸發快取未命中的任何變更宣告為任務的輸入。

尋找任務輸出快取的問題

下面我們描述一個逐步流程,應有助於找出建置中快取的任何問題。

確保增量建置正常運作

首先,請確保您的建置在沒有快取的情況下可以正常運作。在不啟用 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
沒有輸出或沒有輸入的任務將始終執行,但這不應該是問題。

使用如下所述的方法來診斷修復應該是最新的但不是最新的任務。如果您發現某個任務已過時,但沒有可快取的任務依賴於它的結果,那麼您不必對其執行任何操作。目標是為可快取的任務實現穩定的任務輸入

使用本機快取進行原地快取測試

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

執行此測試時,請確保您沒有配置 remote 快取,並且已啟用在 local 快取中儲存。這些是預設設定。

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

$ 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 中進行了簽出。

與先前的測試一樣,您應該沒有配置 remote 快取,並且應啟用在 local 快取中儲存。
$ 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。

若要測試跨平台快取重複使用,請設定 remote 快取(請參閱在 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™ Comparison 工具快速診斷快取未命中。

診斷快取未命中的原因

掌握上一節中的資料後,您應該能夠診斷出為何在建置快取中找不到特定任務的輸出。由於您期望快取更多任務,因此您應該能夠查明哪個建置會產生有問題的成品。

在深入研究如何找出為何未從快取載入某個任務之前,我們應該先研究哪個任務導致了快取未命中。如果建置中較早的任務之一未從快取載入且具有不同的輸出,則會產生級聯效應,導致相依任務執行。因此,您應該找到第一個已執行的可快取任務,並從那裡繼續調查。這可以從 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 方法儲存的屬性檔案包含時間戳記,因此每次建置執行時,都會導致執行階段類別路徑發生變更。為了解決此問題,請參閱不可重複的任務輸出或使用輸入正規化

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