性能即特性

目录

  • Gradle 与 Maven
  • 提升构建启动时间
  • 更快的 IDE 集成
  • 更多精彩,敬请期待!
  • 介绍

    在 Gradle 公司,我们认真对待构建性能。虽然我们在每个 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 构建的固定成本
    • 减少测试执行时间;即,与仅在 IDE 中执行测试相比,减少 Gradle 的开销
    • 提高在 IDE 中导入项目的性能
    • 减少交互式 Gradle 客户端和守护进程之间的通信延迟

    减少配置时间 #

    以下是您可以期望的改进示例

    many empty.png

    因此,上面的示例产生了一个典型的性能测试指标:我们比较了当我们为包含大量子项目(10000 个项目)的项目运行 gradle help 时构建的平均执行时间。您可以看到,当我们开始优化配置时间时,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 testmvn clean test 所需的时间。正如我们前面提到的,在 Gradle 中清理不是必需的,但我们在此处这样做是为了与 Maven 进行比较,并评估“冷构建”时间。以下是结果

    gradle vs maven clean build.png

    在 2 月底,Maven 和 Gradle 的性能相当。从那时起,Gradle 2.13 中的新性能改进导致速度提高了 10%!您可能会注意到该图包含一些小故障:在 4 月 2 日,您可以看到时间显着增加。然而,在 Maven 和 Gradle 两种场景下,时间都增加了。因此,当您阅读此类图表时,您需要记住的是,结果在同一日期之间是相对的。这很重要,因为

    • 我们可以在性能构建的两次执行之间更改模板,从而导致构建时间增加或减少。
    • 我们可以在两次执行之间更改硬件,从而导致相同的副作用

    性能分析胜过猜测 #

    那么我们是如何设法改进这一点的呢?首先,一旦编写了场景并运行了性能测试,我们需要分析构建。为此,我们使用了不同的工具,从 YourKit Java ProfilerJava Mission Control、JIT 日志或只是简单的旧 System.out.println 语句。最后,我们尝试确定导致速度减慢的原因,并编写一份总结我们发现的文档。这些文档都是公开的,您可以在我们的 GitHub 仓库中找到它们。 一旦我们确定了热点并写下了性能分析结果,我们就会提取改进的故事,并实际进入实施阶段。这个“性能分析到故事”阶段非常重要,因为虽然性能分析器对于识别热点非常有帮助,但在解释结果时却无济于事:通常,重写算法可能比尝试优化 SAX 解析器更有效……

    优化守护进程与客户端之间的通信 #

    正如我们解释的那样,我们主要(但不仅限于)专注于在激活守护进程时提高性能。守护进程的一个问题是您有一个分叉的 JVM。当您运行 gradle 时,客户端进程(来自命令行的一个)开始与一个长期运行的进程(守护进程)通信,该进程实际上正在执行构建。通常,要查看构建运行时的日志,您需要将事件从守护进程转发到客户端。在 2.13 之前,此通信是同步的。这意味着日志消息在守护进程和客户端之间同步发送。这是低效的,因为我们在网络 I/O 上阻塞了,而我们实际上可以执行一些构建操作。在 2.13 中,通信不仅是异步的,而且我们还优化了用于在客户端和守护进程之间通信的协议以及客户端如何响应这些事件。

    分叉进程启动更快 #

    在以下场景中,可以看到另一项改进

    gradle vs maven cleanTest test.png

    此场景对 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 个项目具有相同的构建文件但在不同的位置,则脚本只会编译一次。但是,为了能够在正确的构建文件上报告构建错误,我们还使用了一种“重定位技术”,该技术采用已编译的脚本类并将其重新映射到实际的脚本文件,以便正确报告错误。

    优化的类路径 #

    在 2.13 中完成的另一项工作是改进 Gradle 的类路径,以便更快地定位服务。当您的类路径上有大量 jar 时,排序很重要,类数量也很重要。即使您“仅”获得 10 毫秒,当经常执行构建时,特别是从 IDE 执行构建时,也可能会导致显着差异,这导致了我们在 2.13 中工作的最后一项改进领域。

    错误修复 #

    有时,提高性能是偶然的。我们最近发现,某些性能测试在我们的 CI 服务器上执行速度明显快于本地,但不确定原因。在进行一些性能分析后,我们意识到将属性从各种 gradle.properties 文件传播到实际 Project 的代码非常低效:您的各种 gradle.properties 文件中的属性越多,启动构建所需的时间就越长!我们确定了问题并修复了它。

    更快的 IDE 集成 #

    Tooling API 通常允许 IDE 供应商集成 Gradle。这就是我们使用 Buildship 所做的事情。它有非常具体的需求,特别是,它必须同时向后和向前兼容,这意味着 TAPI 的某个版本可以为 Gradle 的旧版本和新版本执行 Gradle 构建。当然,开发人员只有通过同时使用最新版本的 Tooling API 和 Gradle 才能从最新改进中受益,但这会导致有趣的架构。

    在这种情况下,Tooling API 严重依赖反射来调用方法。在 Gradle 2.13 中,我们显着改进了缓存,这带来了惊人的结果

    tapi.png

    此场景说明了将典型的 500 个子项目构建导入 Eclipse 需要多长时间。虽然使用 2.12 版本的 Tooling API 需要 25 秒,但现在只需要 10 秒。您甚至可以在 IntelliJ IDEA 中看到更惊人的结果,他们在其中使用“自定义模型”。导入/同步项目然后将快几个数量级。

    更多精彩,敬请期待! #

    如果不说明我们所说的“什么都不做更好”是什么意思,我们就无法结束这篇博文。在上面的 Maven 与 Gradle 示例中,我们尝试使用 Gradle “模拟” Maven 的行为。以下是通常在 Gradle 中运行正确的增量构建时获得的图表。也就是说,您打开并编辑来自不同子模块的多个文件,然后重新执行测试。请记住,使用 Gradle,您不再需要 clean,但我们很公平,Maven 也没有清理

    maven vs gradle incremental.png

    是的,在这种情况下,Gradle 快了近 6 倍。所以现在,想象一下每天这样做 10 次、100 次,再乘以您公司中开发人员的数量。并意识到这是多少

    感谢您的阅读,请不要担心:更多精彩,敬请期待,请保持联系以获取 Gradle 2.14 中更多的性能改进!

    讨论