性能即功能
在 Gradle Inc.,我们非常重视构建性能。虽然我们在每个 Gradle 版本中都包含了性能改进,但我们从 Gradle 2.13 开始启动了一项名为“性能爆发”的协同努力,旨在让所有用户的软件构建更快、更愉快。在这篇博文中,我们将探讨我们如何处理性能问题,以及在 2.13 版本 及以后版本中可以期待哪些改进。
最快的做法是什么都不做
构建软件需要时间,这就是为什么最大的性能改进是完全省略构建步骤的原因。这就是为什么与 Maven 或 Ant 等传统构建工具不同,Gradle 专注于增量构建。为什么在你不需要的时候要运行 clean
?对于一些开发人员来说,运行 clean
成了对损坏的构建工具的条件反射。Gradle 没有这样的问题:它了解任务的所有输入和输出,能够可靠地处理增量构建。大多数构建将是增量的,这就是为什么我们如此重视优化这种情况的原因。我们实现这一点的一种方法是通过 Gradle 守护进程。
使用 Gradle 守护进程 可以显著提高构建性能,因为它允许构建数据在构建调用之间保存在内存中,并避免每次构建时的 JVM 启动时间。守护进程是一个运行 Gradle 运行时的热 JVM,使后续构建能够更快地运行:我们可以利用缓存的 JVM 的所有优点,而不是为每个构建生成一个新的 JVM——特别是,我们可以从 JIT(即时编译)中获得很大的益处。虽然开启守护进程会对第一次构建造成一定成本,但你将在每次后续构建中节省的时间远远超过初始成本。在 Gradle 2.13 中,我们专注于在激活守护进程时进行改进,并准备在 Gradle 3.0 中默认启用它。我们实施的其他性能改进将使所有用户受益,无论他们是否使用守护进程(如果你还没有使用守护进程,我们强烈建议你尝试一下!)。
正如你可以在我们的 发行说明 中看到的那样,我们强调了几类性能改进
- 减少构建配置时间,也就是说,减少创建和配置 Gradle 构建的固定成本
- 减少测试执行时间;即减少 Gradle 相比在 IDE 中直接执行测试的开销
- 提高在 IDE 中导入项目的性能
- 减少交互式 Gradle 客户端和守护进程之间的通信延迟
减少配置时间
以下是你可能期待的改进
因此,上面的例子产生了一个典型的性能测试指标:我们比较了在运行 gradle help
时,对于包含大量子项目(10000 个项目)的项目,构建的平均执行时间。你可以看到,当我们开始优化配置时间时,master
分支比 Gradle 2.7 慢。现在,Gradle 2.13 比以往任何时候都快!我们在自己的构建中测量了高达 25% 的减少!然而,比改进更重要的是我们是如何实现这一点的。提高性能是一个过程,以下是它的工作原理。
性能测试套件
Gradle 源代码包含一个 专门用于性能测试的子项目。 这个测试套件非常特别,它允许我们
- 比较
master
分支与之前版本的 Gradle 的性能 - 比较针对单个版本的 Gradle 的各种构建场景
因此,通常情况下,在上面的例子中,我们比较了在特定场景(包含 10000 个子项目的空构建)中运行 gradle help
时,构建的平均执行时间,并将其与之前的 Gradle 版本进行比较。值得注意的是,这个性能测试套件每天都会执行,使我们能够在开发阶段的早期发现性能回归。
编写性能测试场景
那么,在实践中,我们如何编写性能测试呢?一切都从我们想要测试的场景开始。例如,我们希望确保减少测试执行的持续时间。第一步是编写一个构建模板,让我们能够针对此场景测试 Gradle。模板具有各种参数:子项目的数量、源代码中的(测试)类数量、外部依赖项等。这让我们能够生成用于衡量性能的示例 Gradle 构建。当然,这些 性能测试构建是使用 Gradle 生成的。
您在下面看到的图表都是使用完全自动化的性能测试生成的,旨在测试特定场景。如果您发现 Gradle 的性能问题,这是一个很好的入门方法:创建一个新的模板,然后 向我们发送一个拉取请求以展示问题。 当然,我们所有的性能测试都是常规测试用例,这意味着如果我们引入回归,我们可以使构建失败。
由于 Gradle 2.13 主要是一个性能增强版本,因此让我们关注一些改进。
Gradle 与 Maven
在此场景中,我们比较了执行 gradle clean test
与 mvn clean test
所花费的时间。如前所述,Gradle 中不需要清理,但我们在这里这样做是为了与 Maven 进行比较,并评估“冷构建”时间。以下是结果
在 2 月底,Maven 和 Gradle 相当。从那时起,Gradle 2.13 中的新性能改进导致速度提高了 10%!您会注意到图表中有一些故障:在 4 月 2 日,您可以看到时间显着增加。但是,Maven 和 Gradle 的两种场景都增加了。因此,在阅读此类图表时,您需要记住的是,结果在同一日期的它们之间是相对的。这是很重要的,因为
- 我们可以在两次执行性能构建之间更改模板,从而导致构建时间增加或减少。
- 我们可以在两次执行之间更改硬件,从而导致相同的效果
分析比猜测更好
那么我们如何设法改进呢?首先,一旦场景编写好并且性能测试运行,我们就需要分析构建。为此,我们使用不同的工具,从 YourKit Java Profiler 到 Java Mission Control,JIT 日志或简单的旧 System.out.println
语句。最后,我们尝试确定导致速度下降的原因,并编写一份总结我们发现结果的文档。这些文档都是公开的,您可以在我们的 GitHub 存储库 中找到它们。一旦我们确定了热点并写下了分析结果,我们就会提取改进的故事,并真正进入实施阶段。这个“从分析到故事”的阶段非常重要,因为虽然分析器在识别热点方面非常有用,但在解释结果方面却毫无帮助:通常,重写算法比尝试优化 SAX 解析器效率更高……
优化守护进程和客户端之间的通信
正如我们所解释的,我们主要(但不仅)专注于在守护进程激活时提高性能。守护进程的一个问题是您有一个分叉的 JVM。当您运行 gradle
时,客户端进程(即来自命令行的进程)开始与一个长生命周期的进程(守护进程)进行通信,该进程实际上执行了构建。通常,要查看构建运行时的日志,您需要将事件从守护进程转发到客户端。在 2.13 之前,这种通信是同步的。这意味着日志消息是在守护进程和客户端之间同步发送的。这效率低下,因为我们在网络 I/O 上阻塞,而实际上我们可以执行一些构建操作。在 2.13 中,不仅通信是异步的,而且我们还优化了用于在客户端和守护进程之间进行通信的协议以及客户端如何响应这些事件。
分叉进程启动速度更快
另一个改进体现在以下场景中
这种情况对 Gradle 来说是“不公平的”,旨在比较当我们只想重新执行测试时会发生什么。您可能知道,当运行 mvn test
时,Maven 即使没有任何更改也会重新执行测试。Gradle 在这种情况下不会做任何事情,因为一切都“最新”。因此,为了模拟 Maven 的行为,我们需要清理测试结果,以便重新执行测试并重新生成报告。如您所见,在这种情况下,Gradle 比 Maven 慢得多。现在,它更快了,同时还做了更多工作:Gradle 不仅运行测试,还生成 3 种类型的报告:二进制报告、XML 报告(用于 CI 集成)以及最终的 HTML 报告(供我们这些可怜的人使用,但您可以 禁用此行为。)Gradle 2.12 在这种情况下慢了 15%,并且通过优化用于测试的分叉 JVM 的类路径,已经做出了大量的改进。在 2.12 中,几乎整个 Gradle 类路径都用于分叉 VM,而实际上我们只需要 Gradle 类的一个子集(基本上用于在分叉 VM 和守护进程之间进行通信)。通过优化此类路径,我们现在可以减少类路径扫描并显着缩短执行测试所需的时间。如果您曾经注意到 Gradle 即将执行测试时出现“暂停”,现在它已经消失了!
报告并行生成
测试执行的改进部分也得益于并行生成报告。正如我们所解释的,Gradle 默认情况下会生成比 Maven 更多的报告。这通常是你想要的,因为当你开发应用程序并在本地运行测试时,解读 XML 测试报告会非常令人沮丧。现在,使用 Gradle 2.13,HTML 和 XML 报告可以并行生成,这显著减少了在开始下一个项目的测试套件之前所需的时间。你的项目模块越多,你越有可能看到构建持续时间的显著减少。
改进构建启动时间
更快的脚本编译
首次执行 Gradle 构建时,你可以在“配置”阶段看到 Gradle 实际上正在编译构建脚本。尽管是脚本,但 Gradle 构建文件是用 Groovy 编写的,并且仍然会被编译成字节码。这很耗时,但 Gradle 团队已经对其进行了优化。特别是,Gradle 必须使用不同的类路径多次编译脚本,以便编译包含对远程资源(如插件)的引用的脚本。
在 Gradle 2.13 中,我们改变了 Gradle 脚本的编译方式,并优化了两种场景:
- 从同一目录并发运行多个构建(这在 CI 中经常发生)。在此之前,Gradle 使用的“脚本缓存”在构建执行期间被锁定,因此如果在构建执行期间更改了构建脚本,所有并发构建都会被锁定,直到第一个构建完成。
- 独立于其位置重用构建脚本。假设你有多个项目使用相同的远程脚本。这在企业环境中很常见,其中一个脚本定义了一些凭据、约定或插件,用于公司所有构建。然后,每个项目都必须在能够使用脚本之前编译它。Gradle 2.13 改变了这一点,现在根据脚本的实际内容(和类路径)而不是其位置来编译脚本。这意味着如果你有两个项目具有相同的构建文件,但位于不同的位置,该脚本只会编译一次。但是,为了能够在正确的构建文件中报告构建错误,我们还使用了一种“重定位技术”,它将编译后的脚本类重新映射到实际的脚本文件,以便正确报告错误。
优化类路径
在 2.13 中完成的另一项工作是改进 Gradle 的类路径,以便更快地定位服务。当类路径上有大量 jar 文件时,排序很重要,类数量也很重要。即使你“只”获得了 10 毫秒的提升,当构建经常执行时,尤其是在 IDE 中,这也会导致显著的差异,这导致了我们在 2.13 中进行改进的最后一个领域。
错误修复
有时,提高性能是偶然的。我们最近发现,一些性能测试在我们 CI 服务器上的执行速度明显快于本地,但我们不确定原因。经过一些分析,我们意识到将属性从各种 gradle.properties
文件传播到实际 Project
的代码效率非常低:在你的各种 gradle.properties
文件中,属性越多,启动构建所需的时间就越长!我们找到了问题并修复了它。
更快的 IDE 集成
工具 API 通常允许 IDE 供应商集成 Gradle。这就是我们对 Buildship 的做法。它有非常具体的需求,特别是它必须向后和向前兼容,这意味着 TAPI 的某个版本可以执行 Gradle 构建,用于 Gradle 的较旧版本和较新版本。当然,开发人员只有在使用最新版本的工具 API 和 Gradle 时才能从最新的改进中受益,但这会导致有趣的架构。
在这种情况下,工具 API 严重依赖于反射来调用方法。在 Gradle 2.13 中,我们显著改进了缓存,这带来了惊人的结果。
这种情况说明了将通常包含 500 个子项目的构建导入 Eclipse 所需的时间。虽然使用 2.12 版本的工具 API 需要 25 秒,但现在只需要 10 秒。你甚至可以在 IntelliJ IDEA 中看到更惊人的结果,因为它们使用“自定义模型”。导入/同步项目的速度将快几个数量级。
还有更多内容即将推出!
我们不能在没有说明“不做任何事更好”的含义的情况下结束这篇文章。在上面的 Maven 与 Gradle 示例中,我们尝试用 Gradle “模拟” Maven 的行为。通常情况下,在使用 Gradle 进行适当的*增量*构建时,您会得到这样的图表。也就是说,您打开并编辑来自不同子模块的多个文件,然后重新执行测试。请记住,使用 Gradle,您不再需要 clean
,但我们很公平,也没有用 Maven 进行清理。
是的,在这种情况下,Gradle 的速度几乎是 Maven 的 6 倍。现在,想象一下,每天做 10 次、100 次,再乘以公司中开发人员的数量。然后意识到这将节省多少资金。
感谢您的阅读,请放心:还有更多内容即将推出,请继续关注 Gradle 2.14 中的更多性能改进!