配置缓存状态 - 通往 Gradle 9 之路

探索 Gradle 如何通过引入配置缓存(Configuration Cache)功能来加快和提高构建效率,该功能通过缓存和重用配置阶段的结果显著缩短构建时间。

目录

简介 #

随着 Gradle 9.0 的临近,我们正在分享配置缓存的最新进展——这是一个显著改善大型项目配置时间的关键功能。在此主要版本中,我们计划将配置缓存设为首选执行模式,目标是在 Gradle 10.0 中默认启用它。

配置缓存是 Gradle 最受期待的功能之一;其进展显著。正如 Gradle 研究员 Tony Robalik 所说

“我对 Gradle 在使配置缓存稳定并成为运行构建的首选机制方面所做的一切工作感到兴奋。在工作中,我们观察到全局启用配置缓存每年可节省约 4 年的工程时间。配置缓存还为独立项目(Isolated Projects)奠定了基础,这是 Gradle 多年前首次宣布以来一直备受期待的功能!”。

阅读这篇博文,了解更多关于配置缓存的近期性能和兼容性改进、如何采用它以及我们未来版本的计划。

历史回顾 #

开发通常是小步进行的——你编写一些代码,运行测试,修复故障,然后重复。在构建工具中,这意味着重复执行相同的任务。

Gradle 的执行模型包含三个阶段

  1. 初始化(Initialization)——发现项目结构。
  2. 配置(Configuration)——构建任务图。
  3. 执行(Execution)——运行任务以执行实际工作。

在增量工作流中,当请求相同的任务而构建脚本没有变化时,配置阶段通常每次都会生成相同的任务图。

Build Cache

Gradle 在构建缓存方面表现出色,它缓存了执行阶段。这使得即使是大型项目和单一仓库,在没有重大更改时也能在几秒钟内完成构建。然而,在配置阶段为复杂项目生成任务图可能会花费相当长的时间。在某些情况下——如以下截图中所示的项目——这种开销甚至可能超过实际执行时间。

This Build Scan shows that the configuration phase takes 26 seconds out of total 32 seconds

重复创建任务图效率低下。即使对于配置时间为 5-10 秒的较小项目,也可能足以使总构建时间超过神奇的 10 秒边界,打破开发人员的注意力。消除这种不必要的工作有助于保持流畅的开发体验。

配置缓存登场 #

认识到这种低效率,Gradle 在 Gradle 6.6 中引入了配置缓存作为实验性功能。

Configuration Cache

启用配置缓存后,任务图在首次运行时计算并存储。在后续构建中,当调用相同任务时,Gradle 会检索缓存的任务图,验证其在当前环境中的有效性,并跳过配置阶段——直接进入执行阶段。正如你所料,加载缓存的任务图比从头开始重新构建要快得多。

Storing and Loading the Configuration Cache

与许多工程优化一样,这种性能提升是有代价的。为了启用缓存,构建脚本和插件必须遵循严格的规则,这些规则允许 Gradle 序列化和反序列化缓存图。这通常需要最终用户和插件开发人员付出努力以确保兼容性。然而,除了启用配置缓存之外,这些约束还增强了任务隔离,从而实现更安全的并行执行——即使对于同一项目中的任务也是如此。

The Configuration Cache in Action

Gradle 8.1 中,配置缓存被提升为稳定功能。从那时起,其 API 遵循与其他 Gradle 功能相同的兼容性保证。

Gradle 团队致力于使核心插件和 Gradle 本身完全兼容配置缓存。我们还与社区插件维护者和其他利益相关者合作,改进和提高配置缓存的采用。

关注易于采用和可观察性 #

插件兼容性 #

我们看到 Gradle 插件生态系统中对配置缓存支持的采用率不断提高。在 2024 年 1 月至 11 月期间,按下载量衡量,50 个最受欢迎的 Gradle 插件中,超过一半已经声明兼容性。数百名维护者和贡献者投入了时间,包括专门的黑客马拉松Hacktoberfest期间的精选项目。

知名插件,如Android Gradle PluginKotlin Gradle PluginSpring Boot,长期以来一直支持配置缓存。其他插件,如Quarkus,在采用配置缓存方面取得了显著进展。

我们对核心插件进行了重大改进,以下是目前已兼容配置缓存的插件列表

JVM 语言和框架 原生语言 代码分析 实用工具
✅ Java ✅ C++ 应用程序 ✅ Checkstyle ✅ 构建初始化
✅ Java 库 ✅ C++ 库 ✅ CodeNarc ✅ 签名
✅ Java 平台 ✅ C++ 单元测试 ✅ JaCoCo ✅ Java 插件开发
✅ Groovy ✅ Swift 应用程序 ✅ JaCoCo 报告聚合 ✅ Groovy DSL 插件开发
✅ Scala ✅ Swift 库 ✅ PMD ✅ Kotlin DSL 插件开发
✅ ANTLR ✅ XC 测试 ✅ 测试报告聚合 ✅ 项目报告插件
✅ WAR 和 EAR      
⚠️ Maven 发布      

对于社区插件,GitHub Issue 跟踪了它们的配置缓存兼容性状态。

虽然采用率稳步增长,但仍有大量工作要做,插件维护者需要您的帮助!Gradle 团队随时准备在 Gradle 社区 Slack 上的#configuration-cache 频道中为贡献者提供拉取请求审查和指导。

报告改进 #

我们持续改进配置缓存报告,使其成为诊断配置缓存问题的重要工具。最近的更新增强了可用性和可见性,提供了更精确的缓存未命中洞察,并突出了被声明为 notCompatibleWithConfigurationCache 的任务。

Configuration Cache Report

我们还在通过更明确的 CLI 摘要、改进的堆栈跟踪以精确定位问题以及更详细的详细/调试输出来完善 Gradle 的内置故障排除工具。随着我们增强性能和兼容性洞察,可观察性仍然是关键焦点。

我们计划对构建输入管理进行多项改进,以提高可见性和控制力

  • 更精确的输入源跟踪 – 报告将超越粗粒度位置(如构建脚本或插件),帮助识别特定输入的精确来源。
  • 更强的防范未察觉的构建输入 – 工程师将拥有更好的工具来检测意外输入,而无需手动解析 HTML 报告。
  • 更好的集成 – 我们正在努力与 Problems API 和 Gradle 构建扫描(Build Scan®)进行更深层次的集成,以简化诊断和洞察。

与 Gradle 构建扫描(Build Scan®)集成 #

Gradle 构建扫描(Build Scan®)提供了对配置缓存使用情况的更深入洞察。你可以看到缓存的任务图是否被重用、缓存条目的大小以及在缓存命中时最初生成它的构建

This Build Scan shows the Configuration Cache hit

Gradle 和 Develocity 的最新版本现在显示缓存未命中的原因

This Build Scan shows the Configuration Cache miss reason

可抑制的输入类型 #

Gradle 跟踪在配置阶段访问的各种文件、环境变量和其他外部因素,以确保缓存的任务图保持 UP-TO-DATE。这些构建配置输入提高了构建的正确性,但最初可能会降低缓存命中率,直到脚本和插件适应。

Gradle 8.3 引入了一项策略,允许新的输入类型至少在一个主要版本中可被抑制,以平衡兼容性和性能。这意味着构建可以选择不检测 Gradle 8.x 中引入的新输入,直到至少 Gradle 9,这为团队提供了更多过渡时间。

关注性能和命中率 #

IDE 体验 #

我们与 JetBrains 合作,增强了测试启动体验。从 Gradle 8.4 和 IntelliJ IDEA 2023.3 开始,当在同一测试任务中运行不同的测试时,可以重用配置缓存条目。例如,运行 FooTest 会存储缓存条目,该条目可以在运行 BarTest 时重用。

在 Gradle 8.7 中,我们修复了 IDEA 生成的运行 public static void main() 方法的任务的配置缓存兼容性问题。后来,IntelliJ IDEA 2024.3 进一步提高了在运行和调试应用程序和测试之间交替时的缓存命中率。因此,许多日常开发任务现在运行速度显著加快。

大小优化 #

大型项目的配置缓存条目会占用大量磁盘空间。Gradle 8.10 通过数据去重改进了存储格式。我们用于基准测试的合成项目的缓存条目从 270 MB 缩小到 65 MB,缩小了四倍。

例如,Google 报告称 AndroidX 构建的配置缓存大小减少了 3.75 倍。虽然这导致了数据序列化带来的存储时间增加,但以下功能解决了这一开销。这种减少显著改善了加载时间,这至关重要,因为在配置缓存命中期间会多次加载缓存

Configuration Cache string de-duplication strategy

并行配置存储和加载 #

使用配置缓存,Gradle 在存储任务图时解析构建依赖项。以前,这是在单个线程上完成的,以保持通常的并发保证——确保配置阶段始终顺序运行。然而,随着目前处于预 alpha 阶段的独立项目的引入,并行配置现在成为可能。我们计划在今年晚些时候将独立项目作为一项孵化功能推出,这可能会带来进一步的改进。

这种单线程方法最初使得使用配置缓存的构建比不使用--parallel的构建慢。相比之下,--parallel通常可以并行解析不同项目中任务的依赖项,从而提高性能。

从 Gradle 8.11 开始,并行配置缓存允许并行存储不同项目的任务图,显著加快了缓存条目准备。与并行任务执行类似,此功能是选择加入的,因为它在以前是单线程执行的地方引入了额外的并发性。然而,已经使用并行执行的构建通常可以安全地启用并行配置缓存。

由于隔离性,恢复缓存图本质上更安全,因此默认启用并行加载。一位早期采用者报告称,一个包含约 600 个项目的构建的配置时间减少了 50%,从 2 分 4 秒缩短到 55 秒,同时缓存大小从 700 MB 减少到 400 MB。

对于我们用于基准测试的合成项目,存储(缓存未命中构建的配置阶段从 27 秒变为 15 秒)和加载(缓存命中构建的配置阶段从 3.6 秒变为 1.5 秒)都实现了两倍的速度提升。

Configuration Cache store and load times

9.x 及更高版本的倡议 #

首选执行模式 #

从 Gradle 9.0 开始,配置缓存将成为首选执行模式。Gradle 将温和地提示尚未启用它的构建,并且使用 gradle init 创建的新项目将默认启用它。这种转变奠定了未来可扩展性改进的基础,例如独立项目,同时也简化了 Gradle 代码库。

然而,我们还不能普遍启用配置缓存。有些构建依赖于在启用它时可能不成立的假设——例如期望任务顺序运行或在任务之间共享可变 Java 对象。虽然 Gradle 会检测到许多此类情况并将其报告为错误,但某些模式,即使有效,也可能表现出不同的行为。启用配置缓存仍然是一个深思熟虑的选择,以避免中断现有构建。

虽然最初的计划是弃用不使用配置缓存的构建,但我们选择了一种更渐进的方法——鼓励采用,同时为需要调整的项目留出时间。我们的长期目标保持不变:在 Gradle 9.0 之后的未来主要版本中,使配置缓存成为唯一的执行模式。

我们将在即将发布的版本中弃用并移除与配置缓存不兼容的 API,以支持这种转变。这使得编写不合规的代码变得更加困难,即使您的构建尚未准备好采用它。有些问题只能在使用配置缓存本身时检测到,因此请将其报告的任何错误视为弃用警告,即使在禁用配置缓存时它们没有出现。

Gradle 将继续改进整个生态系统对配置缓存的支持,我们鼓励所有项目现在就启用它,以利用其性能和可维护性优势。

当前不兼容的功能 #

一些 Gradle 功能和内置插件尚未完全兼容配置缓存。

  • 源依赖
    • 源依赖是 Gradle 中一个鲜为人知的功能,它允许包含来自 Git 仓库的构建。我们计划在 9.x 时间线内支持此功能与配置缓存。在此期间,您可以评估社区提供的替代方案
  • Ant 集成
    • 使用 Ant 项目并通过 Gradle 运行 Ant 任务将不被配置缓存直接支持。我们计划用一个功能集缩减的兼容解决方案替换当前的集成。在此期间,我们鼓励开发人员完成从 Ant 构建的迁移。
  • IDE 插件
    • IDE 插件,如 IDEAEclipse,支持配置导入项目和生成项目文件。这些插件尚未完全支持配置缓存。现代 IDE 版本无需 Gradle 生成项目文件即可导入 Gradle 项目,因此此功能将被弃用和移除。但是,这些插件提供的配置项目和底层工具模型(例如将某些源目录标记为测试)的支持将保留并兼容。

我们也意识到其他功能中存在一些粗糙的边缘和极端情况,我们最终希望解决这些问题,尽管它们并未阻止大多数构建使用配置缓存。您可以查看Gradle 路线图条目以查看正在进行的工作。

提高性能和缓存命中率 #

到目前为止,配置缓存的行为类似于任务的 UP-TO-DATE 检查——它只缓存给定任务的最新调用。这意味着来回更改环境每次都会使缓存失效。

例如,您可能运行测试,提升依赖版本,重新运行测试,然后决定新版本不起作用。如果您回滚更改并重新运行测试,您将不会获得配置缓存命中,即使您之前运行了相同的配置。发生这种情况是因为使用更新的依赖项运行测试会覆盖先前的缓存条目。

我们计划通过允许为同一 Gradle 调用存储多个缓存条目来尽快解决此问题。当在基于 main 分支的相同修订版上跨多个分支工作时,这将很有用,从而实现它们之间更平滑的切换。

CI 上的配置缓存 #

配置缓存的当前优先级是加快本地增量构建。早期的设计决策优先考虑本地性能,有时这使得 CI 采用更具挑战性。

在 Gradle 8.11 之前,顺序依赖解析会降低配置缓存构建的速度,尤其是在临时 CI 环境中。通过并行配置缓存,缓存的构建与未缓存的构建表现相同。存储状态的开销通常被项目内并行任务执行所抵消。

然而,在 CI 上实现构建之间的缓存重用是一个不同的挑战。对于保留构建状态的有状态 CI 环境来说,这可能是可行的,但对于临时 CI 和干净环境来说,则更具挑战性。必须克服几个障碍

  • 配置缓存条目不可重定位,因为它们包含许多绝对文件路径。这种限制可以通过确保所有 CI 机器具有相同的检出目录和 GRADLE_USER_HOME 位置来缓解。然而,不可重定位性也使得在开发人员之间共享缓存数据变得困难。
  • 当包含的构建或 buildSrc 对构建逻辑有所贡献时,Gradle 期望所有它们的输出都存在才能命中缓存。

我们认识到 CI 上的缓存重用很重要,并有很多改进情况的想法。然而,这项工作仍处于早期阶段,并且存在许多技术挑战,因此时间线尚未确定。

尽管如此,如果您在本地使用配置缓存,我们仍建议在 CI 构建上启用它。这样,您可以更早地发现新引入的破坏,并受益于项目内并行执行,但不要过于关注实现缓存重用。

为配置缓存准备项目 #

配置缓存是所有 Gradle 用户的一项基本功能,需要 Gradle 生态系统进行重大更新才能发挥其全部潜力。我们鼓励社区,包括插件维护者和最终用户,投入一些时间在其插件和构建脚本中支持配置缓存。

如果您尚未这样做,现在也是一个尝试在您的构建中启用配置缓存的好时机。如果您遇到任何插件兼容性问题,请在相应的仓库中报告它们,并在兼容性跟踪器中引用它们。

您也可以在我们的论坛Gradle 社区 Slack#configuration-cache 频道中与我们联系。我们乐于为致力于配置缓存兼容性的开发人员提供建议和评审!

如何提供帮助? #

插件生态系统仍需要大量工作,插件维护者需要您的帮助!
如果您有兴趣贡献,以下是您可以参与的方式

讨论