Configuration Cache 的状态 - Gradle 9 之路

随着 Gradle 9.0 的临近,我们正在分享关于 Configuration Cache 的最新进展——这是一项关键特性,可以显著改善大型项目的配置时间。

目录

介绍

随着 Gradle 9.0 的临近,我们正在分享关于 Configuration Cache 的最新进展——这是一项关键特性,可以显著改善大型项目的配置时间。在这个主要版本中,我们计划使 Configuration Cache 成为首选执行模式,目标是在 Gradle 10.0 中默认启用它。

Configuration Cache 是 Gradle 最受期待的特性之一;进展显著。正如 Gradle Fellow Tony Robalik 指出的那样

“我对 Gradle 在使 Configuration Cache 稳定并成为构建的首选机制方面所做的一切工作感到兴奋。在工作中,我们观察到全局启用 Configuration Cache 每年将挽回大约 4 年的工程时间。Configuration Cache 也为 Isolated Projects 奠定了基础,自从 Gradle 首次宣布它以来,Isolated Projects 一直是一项令人期待已久的特性!”。

阅读这篇文章,了解更多关于最近 Configuration Cache 性能和兼容性改进、如何采用它以及我们未来版本计划的信息。

一点历史 #

开发通常以小增量进行——您编写一些代码,运行测试,修复失败,然后重复。在构建工具中,这意味着重复执行相同的任务。

Gradle 的执行模型由三个阶段组成

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

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

Build Cache

Gradle 擅长构建缓存,它可以缓存执行阶段。这使得即使是大型项目和单体仓库也能在几秒钟内构建完成,前提是没有发生重大更改。然而,对于复杂的项目,在配置阶段生成任务图可能需要相当长的时间。在某些情况下——如下面的屏幕截图所示的项目——这种开销甚至可能超过实际执行时间。

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

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

Configuration Cache 登场 #

认识到这种低效性,Gradle 在 Gradle 6.6 中引入了 Configuration Cache 作为一项实验性特性。

Configuration Cache

启用 Configuration Cache 后,任务图在第一次运行时计算并存储。在后续构建中,当调用相同的任务时,Gradle 检索缓存的任务图,验证其对当前环境的有效性,并跳过配置阶段——直接进入执行阶段。正如您可能预期的那样,加载缓存的任务图比从头开始重建它要快得多。

Storing and Loading the Configuration Cache

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

The Configuration Cache in Action

Gradle 8.1 中,Configuration Cache 被提升为稳定版。从那时起,它的 API 就遵循与其他 Gradle 特性相同的兼容性保证。

Gradle 团队一直致力于使核心插件和 Gradle 自身完全兼容 Configuration Cache。我们还与社区插件维护者和其他利益相关者合作,以改进和提高 Configuration Cache 的采用率。

关注易用性和可观察性 #

插件兼容性 #

我们看到 Gradle 插件生态系统中对 Configuration Cache 支持的采用率不断提高。在 2024 年 1 月至 11 月期间下载量排名前 50 的 Gradle 插件中,超过一半已经声明了兼容性。数百名维护者和贡献者投入了他们的时间,包括 专门的黑客马拉松Hacktoberfest 期间的特色项目。

Android Gradle PluginKotlin Gradle PluginSpring Boot 这样的著名插件长期以来一直支持 Configuration Cache。其他插件,如 Quarkus,在采用 Configuration Cache 方面也取得了重大进展。

我们对 核心插件 进行了重大改进,以下是现在与 Configuration Cache 兼容的插件列表

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 跟踪了它们的 Configuration Cache 兼容性状态。

虽然采用率稳步提高,但仍有大量工作要做,插件维护者可以获得您的帮助!Gradle 团队在这里支持贡献者进行 pull request 审查,并在 Gradle 社区 Slack 的 #configuration-cache 频道中提供指导。

报告改进 #

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

Configuration Cache Report

我们还在改进 Gradle 的内置故障排除工具,提供更明确的 CLI 摘要、改进的堆栈跟踪以精确定位问题,以及更详细的 verbose/debug 输出。当我们增强性能和兼容性洞察时,可观察性仍然是关键重点。

我们正在计划对构建输入管理进行多项改进,以增强可见性和控制

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

与 Build Scan® for Gradle 的集成 #

Build Scan® for Gradle 提供了对 Configuration Cache 使用情况 的更深入洞察。您可以查看缓存的任务图是否被重用、缓存条目的大小,以及在缓存命中的情况下,哪个构建最初生成了它

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 开始,当在同一个测试任务中运行不同的测试时,Configuration Cache 条目现在可以被重用。例如,运行 FooTest 存储缓存条目,当运行 BarTest 时可以重用该条目。

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

大小优化 #

大型项目的 Configuration Cache 条目可能会占用大量磁盘空间。Gradle 8.10 通过重复数据删除存储的数据改进了存储格式。我们用于基准测试的 合成项目 的缓存条目从 270 MB 缩小了四倍,降至 65 MB。

例如,Google 报告称 AndroidX 构建的 Configuration Cache 大小减少了 3.75 倍。虽然这带来了由于数据序列化而增加的存储时间,但以下特性解决了这一开销。这种减少显著提高了加载时间,这至关重要,因为缓存在 Configuration Cache 命中期间会被多次加载

Configuration Cache string de-duplication strategy

并行配置存储和加载 #

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

这种单线程方法最初使使用 Configuration Cache 的构建比使用没有它的 --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 开始,Configuration Cache 将成为首选执行模式。Gradle 将温和地提示尚未启用它的构建,并且使用 Gradle init 创建的新项目将默认启用它。这种转变为未来的可扩展性改进(如 Isolated Projects)奠定了基础,同时也简化了 Gradle 代码库。

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

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

我们将弃用和删除与 Configuration Cache 不兼容的 API,以支持即将到来的版本中的这种过渡。这使得编写不兼容的代码更加困难,即使您的构建尚未准备好采用它。某些问题只能在使用 Configuration Cache 本身时才能检测到,因此请将它报告的任何错误视为弃用警告,即使它们在禁用 Configuration Cache 时不会出现。

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

当前不兼容的特性 #

一些 Gradle 特性和内置插件尚未完全兼容 Configuration Cache。

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

我们也意识到其他特性中的一些粗糙之处和极端情况,即使它们并没有阻止大多数构建使用 Configuration Cache,我们也希望最终解决这些问题。您可以查看 Gradle 路线图条目,以了解正在进行的工作。

提高性能和缓存命中率 #

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

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

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

CI 上的 Configuration Cache #

Configuration Cache 当前的首要任务是加速本地增量构建。早期的设计决策优先考虑本地性能,有时使 CI 采用更具挑战性。

在 Gradle 8.11 之前,顺序依赖项解析减慢了 Configuration Cache 构建的速度,尤其是在短暂的 CI 环境中。通过并行配置缓存,缓存的构建与未缓存的构建性能相当。存储状态的开销通常被项目内并行任务执行所抵消。

然而,在 CI 上实现构建之间的缓存重用是另一项挑战。对于保留构建状态的有状态 CI 环境,这可能是可行的,但对于短暂的 CI 和干净的环境,这要困难得多。必须克服几个障碍

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

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

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

为 Configuration Cache 准备您的项目 #

Configuration Cache 是所有 Gradle 用户的基本特性,要充分发挥其潜力,就需要 Gradle 生态系统进行重大更新。我们鼓励社区,包括插件维护者和最终用户,投入一些时间来支持其插件和构建脚本中的 Configuration Cache。

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

您也可以在 我们的论坛Gradle 社区 Slack#configuration-cache 频道上联系我们。我们很乐意为致力于 Configuration Cache 兼容性的开发人员提供建议和审查!

您如何提供帮助? #

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

讨论