Gradle build 加速的 6 个技巧

1,941 阅读8分钟
原文链接: medium.com

6 tips to speed up your Gradle build

Last time when we talked about build systems, we looked at some tips that might make your Maven build faster. The results we got were fascinating and the response was overwhelming. Most people were pretty happy with the speedup that they achieved on their projects from the tips we gave. Today, we’re going to look at what can be done with Gradle builds. The builds of most projects are quite standard, but yet they are unique. Almost all projects add their own complexity into the build. And while they are all different, one thing is common: builds can take up your precious time and speeding it up can affect the developer productivity which makes working on your project more pleasant.

Without further ado, let’s see what Gradle, with its “Realize Build Happiness” motto, is packing in its speed department.

Speeding up Gradle builds

This blogpost is largely inspired by the wonderful session by Madis Pink: Squeezing the Last Drop of Performance Out of Your Gradle Builds. Madis is one of the engineers behind the JRebel for Android project, so if you develop with Android apps you should try it! Madis will be ecstatic about that — but don’t too many of you watch it, we don’t want Madis to become big headed!

For the application under test we’ll use the same code that Madis used, iosched, the de-facto default example Android application. Have no fear, Gradle is the same for your Java project as it is for any Android app. Which means that the advice we’ll give here which speeds up your Gradle builds is applicable to both environments. So you can use all the tricks from this post directly in your Java project.

Before we start any optimizations, we need to first understand that a Gradle build has a lifecycle, which can be split into three distinct phases:

  • Initialization — scanning the projects to find out which ones to build.
  • Configuration — running the build.gradle scripts and building the task graph.
  • Execution — the useful part where Gradle actually builds your app.

Now you can see the pain right away. There’s clearly one useful phase that we might be able to speed up in our own build scripts if the peculiarities of a particular project allow it, and two phases in which Gradle exclusively performs selfish tasks: configuring itself and imposing the execution overhead. In this post we’ll first concentrate on reducing the overhead of the build before we try to make the build itself faster.

Let’s start optimizing the build step by step while measuring the progress. if you want to run the experiments on the iosched app yourself, get it from Github, like this:


git clone http://github.com/google/iosched
cd ioshed

Now we’re ready to rumble! Let’s build the app once for Gradle to obtain the dependencies and to ensure that we have a typical development environment scenario at hands.

Now build the app again, but with the — dry-run flag, which will make Gradle actually skip the execution of all tasks. This means that we’ll configure the Gradle for execution and perform all the tasks it would normally do, just without doing the actual work. It is precisely the overhead that we want to measure and reduce.

Execute the following command a couple of times, as the first time you do this the build will pull down the required dependencies, if you’re using a fresh project. As a result, this first run won’t reflect the regular build speed baseline.


./gradlew :android:assembleDebug --dry-run

After considering all the tasks that Gradle would like to perform, but skips due to the — dry-run flag, it prints how much time it took to run the command. On my poor MacBook pro 2013 (still sticky from the coke I accidentally spilled on it), the build takes almost 9 seconds.


BUILD SUCCESSFUL
Total time: 8.674 secs

A proper measurement technique would require us to run the command multiple times, then remove the outliers and average the results. But as we’re not doing science here, we’ll skip the boring parts. Take it with a grain of salt, your mileage may vary!

The second step is to enable profiling on the Gradle build. Just slap the — profile flag on the Gradle command and you’ll get a nice report saying where the time went.


./gradlew :android:assembleDebug --dry-run --profile
open build/reports/profile/profile-2016-02-02-15-39-17.html

The profile shows that the majority of the time goes into configuring the projects:

So let’s make the configuration faster.

Use configure on demand

There is a straightforward way to reduce that number. We need to make Gradle configure everything on demand rather than eagerly. Luckily for us, it’s just the matter of adding another command line flag: — configure-on-demand.


./gradlew :android:assembleDebug --dry-run --profile --configure-on-demand

The result is a bit better on the eye:


BUILD SUCCESSFUL


Total time: 7.144 secs

The profile shows that with the on demand configuration it takes almost a second less to configure the project: configuring projects — 2.359s. It might seem negligible, but mind you, that’s a 17% speedup! Not bad for such a simple flag.

Configuration on demand is an incubation feature of Gradle, so it’s not enabled by default yet. We might get there one day, but for now you can enable it globally, by adding a line to .gradle/gradle.properties in your home directory. A command like the one below will suffice on Linux / OSX:


echo 'org.gradle.configureondemand=true'>>
~/.gradle/gradle.properties

Use Gradle daemon

Now, since we’re talking about adding global properties, let’s make sure that we use the Gradle daemon too. The Gradle daemon is a background process that doesn’t exit after your Gradle build finishes. Next time you invoke Gradle, it will still be there just waiting to rock your world, or at least your build. This makes a lot of sense, since Gradle is a JVM process that needs to start, load the JVM, load the classes, JIT them and so on. Limiting the impact of all that overhead is exactly what the Gradle daemon does.

Let’s compare the times for our Gradle build with and without the daemon running:


./gradlew :android:assembleDebug --dry-run --no-daemon
# vs. 
./gradlew :android:assembleDebug --dry-run --daemon

On my machine, after a couple of warm ups, the build with the daemon is blazingly fast in comparison to our initial benchmark, and I use that term very loosely:


BUILD SUCCESSFUL
Total time: 2.536 secs

Now our build takes just 29% of the time that it initially took. How cool is that?!
So using the daemon is also universally great, so you should enable it globally as well.


echo 'org.gradle.daemon=true'>>
~/.gradle/gradle.properties

Use the latest Gradle version

Let’s talk about gradle versions. Gradle is a complex beast and it is build on top of several projects that all evolve over time. Most of the projects get faster and faster with every release (I’m looking at you, Eclipse Juno), so it makes sense to use their latest stable versions.

So far in this post we’ve been running Gradle 2.2.1. The latest Gradle release to date is 2.10, so let’s upgrade and use it. With a different build tool, the upgrade process could be painful and annoyingly manual. Gradle is different. Most of the projects make use of Gradle wrapper, the utility that fixes the version of Gradle that the project uses and ensures the repeatability of the builds. It’s a great thing and if your project uses Gradle you should use the wrapper too. For example on his Virtual JUG session, Andres Almiray, Java Champion and a big Gradle fan, advocated for that too! Trust him, he knows a lot about Gradle.

To change the Gradle version in use, when using the wrapper, one just needs to update a number in the wrapper configuration. The configuration sits in the gradle/wrapper/gradle-wrapper.properties file in the project’s home directory. Unfortunately, due to some bug in the tooling chain, Gradle welcomes us with a failure here:


> Failed to apply plugin [id 'com.android.application']
   > Gradle version 2.2 is required. Current version is 2.10. If using the gradle wrapper, try editing the distributionUrl in /Users/shelajev/repo/tmp/iosched/gradle/wrapper/gradle-wrapper.properties to gradle-2.2-all.zip

Obviously, it’s a honest mistake of comparing version numbers as strings, so while we wait for someone to fix it, let’s just assume Gradle 2.9 is the latest one. And let’s see how 2.9 performs.


BUILD SUCCESSFUL
Total time: 1.356 secs

Gradle 2.9 doesn’t disappoint, we’ve got even faster times for the overhead now. Pretty sweet, eh, 8 seconds versus 1.3 now!

The same argument goes for Java versions as well. If you haven’t upgraded yet to Java 8, do it now! Well finish reading this blog post, but do it straight afterwards! You don’t even have to move your project to use Java 8, lambdas and so on. Just make sure your build tool executes with the latest and the most performant Java version out there.

Optimize your project

Until now we mostly talked about shaving yaks on the overhead that the build system imposes onto your build. That’s great, but honestly, most of the benefits that you can reap in the speeding department are hidden inside the actual process of building your process. Well, ok in our example we saved most of the time by eliminating overhead, but just imagine what happens for more realistic project builds. Let’s see how one can speed up Gradle build process for real.

Avoid heavy computations

Usually, during the build, Gradle might do a lot of heavy lifting that can be optimized away, at least partially. Let’s look at our example and try to tweak the IO operations that Gradle performs. For example, you build a typical application and for the purposes of the continuous integration you need to store the information what commit you’re actually building.

Well, that information is just one command away, right? So you have something like this in your gradle.build file:


def cmd = 'git rev-list HEAD --first-parent --count'
def gitVersion = cmd.execute().text.trim().toInteger()
android {
  defaultConfig {
    versionCode gitVersion
  }
}

The code above executes a git command and stores the result in a variable for later use. Peachy, but the actual command execution takes time. For the purposes of your development environment you probably don’t need that information. Luckily, Gradle is really flexible and the configuration is just a pure Groovy file. So if you change the configuration above to something like the example below, not performing unnecessary actions when not in the continuous integration environment, you’d win a second.


def gitVersion() {
  if (!System.getenv('CI_BUILD')) {
    // don't care
    return 1
  }
  def cmd = 'git rev-list HEAD --first-parent --count'
  cmd.execute().text.trim().toInteger()
}
android {
  defaultConfig {
    versionCode gitVersion()
  }
}

I’d bet a shiny penny that there are a load of places in your codebases just like, so to get a really performant build you should consider that hint. Given how easy it is to avoid the computations, there’s no excuse not to do it.

Fix the dependencies

Gradle allows you to specify version ranges for the dependencies that your project is accepting. In the example below, any minor version of the gson 2 will satisfy the dependency constraint. As a matter of fact, Gradle will try to pick the latest available version that it finds. This flexibility comes at a performance cost. Gradle will have to go online and check what versions are available. That can sometimes be unnecessary, and slow, especially if your internet connection is poor. Ever kicked off a build while on a train?


dependencies {
	compile 'com.google.code.gson:gson:2.+'
}

Not only might the dynamic dependencies slow down your build, but you also lose the repeatability of the builds. Your continuous integration environment might find a newer version or maybe weeks after you pushed the commit, someone will fetch the code and wonder what went wrong.

In any case, it is a good idea for the performance of the build to avoid dynamic dependencies and fix the version in place. It’s not that hard to do, just find out what version Gradle downloads and put that number in place.

Modularize the project and parallelize the build

Last, but not least, and, perhaps, the most impactful thing that you can do to your project to increase its build speed is to modularize it better. There are a couple of factors in play here. First of all, the modular projects can be built in parallel. We talked about it when discussed how to speed up Maven, and Gradle is no exception.

To enable parallel builds, which is another incubation feature, you need to supply another command line flag. You can either add the — parallel flag to your Gradle command or enable it globally in the gradle.properties file as we did with the daemon option earlier:


echo 'org.gradle.parallel=true' >> ~/.gradle/gradle.properties

Besides the obvious speedup you’ll get from executing the build over using multiple threads at the same time, you get the following additional benefits:

  • Configuration of projects in parallel.
  • Re-use of configuration for unchanged projects.
  • Project-level up-to-date checks.
  • Using pre-built artifacts in the place of building dependent projects.

The last two points are particularly important. You rarely work all over the place, most probably your changes to the code are nicely contained in a few projects at a time. That means that Gradle will be able to figure out and avoid building unnecessary projects over and over again. And the work that’s not done is the fastest work ever.

Conclusion

In this post we looked at a number of suggestions that Madis Pink recommended in his conference session. If we want to boil it down to a few concise points, here’s a shortlist:

  • Enable Configuration on Demand.
  • Use Gradle Daemon.
  • Newer versions of Gradle are faster, also Java 1.8 is faster than 1.6. Upgrade!
  • Avoid doing expensive things during the configuration phase.
  • Don’t use dynamic dependencies (“x.y.+”).
  • Parallelize the build.

Some of these suggestions will reduce the time that Gradle spends configuring itself, your project, and others like avoiding the dynamic dependencies and parallelizing the execution will make the actual process of building your code faster. And the best thing is that this advice is equally applicable to your Java project and Android apps built by Gradle alike.

I’d be happy to hear if you have other tips that can speed up a Gradle build. Leave me a note in the comments below and I’ll do my best to spread that knowledge around! Also if you try any of the advice from this post, I’d be delighted to hear about the results. Ping me on Twitter: @shelajev and let’s have a chat.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

This post was originally published on RebelLabs: http://zeroturnaround.com/rebellabs/making-gradle-builds-faster/.