性能即是特性

目录

  • Gradle vs Maven
  • 提高构建启动时间
  • 更快的IDE集成
  • 还有更多!
  • 引言

    在Gradle Inc.,我们认真对待构建性能。虽然我们将性能改进捆绑到每个Gradle版本中,但我们从Gradle 2.13开始启动了一项名为“性能爆发”的协同努力,以便为所有用户提供更快、更愉快的软件构建体验。在这篇博文中,我们将探讨我们如何解决性能问题,以及在2.13版本及更高版本中可以期待哪些改进。

    最快的做法是不做任何事 #

    构建软件需要时间,因此最大的性能改进是完全消除其中的步骤。这就是为什么与Maven或Ant等传统构建工具不同,Gradle专注于增量构建。当您不需要时,为什么还要运行clean?对于一些开发人员来说,运行clean已成为对损坏构建工具的条件反射。Gradle没有这样的问题:它了解任务的所有输入和输出,并且能够可靠地处理增量构建。大多数构建都是增量的,这就是为什么我们如此专注于优化这种情况。我们实现这一目标的一种方法是通过Gradle守护进程。

    通过允许构建数据在构建调用之间持续保留在内存中并避免每次构建的JVM启动时间,Gradle守护进程可以显著提高您的构建性能。守护进程是一个热JVM,托管Gradle运行时,使得后续构建能够运行得更快:而不是为每个构建生成新的JVM,我们可以利用缓存JVM的所有优点——特别是,我们从JIT(即时编译)中获得了强大的好处。虽然第一次构建启用守护进程会产生一些成本,但您在每次后续构建中节省的时间将远远超过初始成本。在Gradle 2.13中,我们专注于在激活守护进程时的改进,并且我们正准备在Gradle 3.0中默认启用它。我们实现的其它性能改进将使所有用户受益,无论他们是否使用守护进程(如果您还没有使用守护进程,我们强烈鼓励您尝试一下!)。

    正如您在我们版本说明中所读到的,我们强调了几个类别的性能改进

    • 减少构建配置时间,也就是说,减少创建和配置Gradle构建的固定成本
    • 减少测试执行时间;即,减少Gradle与仅在IDE中执行测试相比的开销
    • 提高在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的性能问题,这是一个很好的开始方式:创建一个新模板,然后发送一个Pull Request给我们展示问题。当然,我们所有的性能测试都是常规的测试用例,这意味着如果我们引入回归,我们可以*使构建失败*。

    由于Gradle 2.13主要是一个性能增强版本,让我们关注一些改进。

    Gradle vs Maven #

    在此场景中,我们比较了执行gradle clean testmvn clean test所需的时间。如前所述,Gradle不需要执行clean,但我们在这里这样做是为了与Maven进行比较,并评估“冷构建”时间。这是结果

    gradle vs maven clean build.png

    在二月底,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.13中完成的另一项工作是改进Gradle的类路径,以便更快地定位服务。当您的类路径上有许多jar文件时,顺序很重要,并且类数量也很重要。即使您“只”节省了10毫秒,当构建经常执行时,这也会导致显着差异,尤其是在IDE中执行时,这导致了我们在2.13中工作的最后一个改进领域。

    Bug修复 #

    有时,提高性能是一个偶然的发现。我们最近发现,在我们的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通常需要多长时间。虽然在Tooling API的2.12版本中需要25秒,但现在只需要10秒。您甚至可以在IntelliJ IDEA中看到更惊人的结果,它们使用“自定义模型”。导入/同步项目将变得数量级更快。

    还有更多! #

    在结束这篇博文之前,我们不能不说明我们所说的“做任何事都比什么都不做好”的意思。在上面的Maven vs Gradle示例中,我们试图用Gradle“模拟”Maven的行为。这通常是运行正确的*增量*构建时得到的图表。也就是说,您打开并编辑了不同子模块中的多个文件,然后重新执行测试。请记住,使用Gradle,您不再需要clean,但我们很公平,并且也没有对Maven执行clean。

    maven vs gradle incremental.png

    是的,Gradle在此场景中的速度几乎是6倍。现在,想象一下每天执行10次,100次,乘以您公司中开发人员的数量。然后意识到这能省下多少*钱*。

    感谢您的阅读,请放心:未来还有更多内容,请继续关注Gradle 2.14的更多性能改进!

    讨论