Faster Maven builds
Tags: java, maven, ci, performance
Published: 2021-10-03 20:10:46

Builds require a few properties, chief among them reproducibility. I would consider speed to be low on the order of priorities. However, it's also one of the most limiting factors to your release cycle: if your build takes _T_, you cannot release faster than each _T_. Hence, you'll probably want to speed up your builds after you've reached a certain maturity level to enable more frequent releases.

I want to detail some techniques you can leverage to make your Maven builds faster in this post. The following post will focus on how to do the same inside of Docker.

## Baseline

Since I want to propose techniques and evaluate their impact, we need a sample repository. I've chosen [Hazelcast code samples](https://github.com/hazelcast/hazelcast-code-samples) because it provides a large enough multi-modules code base with many submodules; the exact commit is [448febd](https://github.com/hazelcast/hazelcast-code-samples/commit/448febd9977b9927a3f00bbf61ba50b2c0d94bb4).

The rules are the following:

* I run the command five times to avoid temporary issues
* I execute `mvn clean` between each run to start from an empty `target` repository
* All dependencies and plugins are already downloaded
* I report the time that Maven displays in the console log:

    ```
    [INFO] -------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] -------------------------------------------------------
    [INFO] Total time:  22.456 s (Wall Clock)
    [INFO] Finished at: 2021-09-24T23:20:41+02:00
    [INFO] -------------------------------------------------------
    ```

Let's start with our baseline, `mvn test`. The results are:

* 02:00 min
* 01:57 min
* 01:58 min
* 01:56 min
* 01:58 min

## Using all CPUs

By default, Maven uses a single thread. In the age of multicores, this is just waste. It's possible to run parallel builds using multiple threads by setting an absolute number or a number relative to the number of available cores. For more information, please check the [relevant documentation](https://cwiki.apache.org/confluence/display/MAVEN/Parallel+builds+in+Maven+3).

The more submodules that are not dependent on each other you have, _i.e._, Maven can build them in parallel, the better you'll achieve with this technique. It fits our codebase very well.

We are going to use as many threads as there are available cores. The relevant command is `mvn test -T 1C`.

When the command starts, you should see the following message in the console:

```
Using the MultiThreadedBuilder implementation with a thread count of X
```

* 51.487 s (Wall Clock)
* 40.322 s (Wall Clock)
* 52.468 s (Wall Clock)
* 41.862 s (Wall Clock)
* 41.699 s (Wall Clock)

The numbers are much better but with a higher variance.

## Parallel test execution

Parallelization is an excellent technique. We can do the same regarding test execution. By default, the Maven Surefire plugin runs tests sequentially, but it's possible to configure it to run tests in parallel. Please refer to the [documentation](https://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html#Parallel_Test_Execution) for the whole set of options.

This approach is excellent if you've got a large number of units in each module. Note that your tests **need** to be independent of one another.

We will manually set the number of threads:

```bash
mvn test -Dparallel=all -DperCoreThreadCount=false -DthreadCount=16 #1 #2
```

1. Configure Surefire to run both classes and methods in parallel
2. Manual override the thread count to 16

Let's run it:

* 02:04 min
* 02:03 min
* 01:46 min
* 01:52 min
* 01:53 min

It seems that the cost of thread synchronization offsets the potential gain of running parallel tests.

## Offline

Maven will check whether a `SNAPSHOT` dependency has a new "version" at every run. It means additional network roundtrips. We can prevent this check with the `--offline` option.

While you should avoid `SNAPSHOT` dependencies, it's sometimes unavoidable, especially during development.

The command is `mvn test -o`, `-o` being the shortcut for `--offline`.

* 01:46 min
* 01:46 min
* 01:47 min
* 01:55 min
* 01:44 min

The codebase has a considerable number of `SNAPSHOT` dependencies; hence offline speeds up the build significantly.

## JVM parameters

Maven itself is a Java-based application. It means each run starts a new <abbr title="Java Virtual Machine">JVM</abbr>. A JVM first interprets the _bytecode_ **and** then analyze the workload and compiles the _bytecode_ to _native code_ accordingly: it means peak performance, but only after a (long) while. It's great for long-running processes, not so much for command-line applications.

We will likely not reach the peak performance point in the context of builds since they are relatively short-lived, but we are still paying for the analysis cost. We can configure Maven to forego it by configuring the adequate JVM parameters. Several ways of configuring the JVM are available. The most straightforward way is to create a dedicated `jvm.config` configuration file in a `.mvn` subfolder in the project's folder.

```
-XX:-TieredCompilation -XX:TieredStopAtLevel=1
```

Let's now simply run `mvn test`:

* 01:44 min
* 01:44 min
* 01:53 min
* 01:53 min
* 01:55 min

## Maven daemon

The [Maven daemon](https://github.com/mvndaemon/mvnd) is a recent addition to the Maven ecosystem. It draws its inspiration from the [Gradle daemon](https://docs.gradle.org/current/userguide/gradle_daemon.html):

> Gradle runs on the Java Virtual Machine (JVM) and uses several supporting libraries that require a non-trivial initialization time. As a result, it can sometimes seem a little slow to start. The solution to this problem is the Gradle Daemon: a long-lived background process that executes your builds much more quickly than would otherwise be the case. We accomplish this by avoiding the expensive bootstrapping process and leveraging caching by keeping data about your project in memory.

The Gradle team recognized early that a command-line tool was not the best usage of the JVM. To fix that, one keeps a JVM background process, the daemon, always up. It acts as a server while the <abbr title="Command-Line Interface">CLI</abbr> itself plays the role of the client.

As an additional benefit, such a long-running process loads classes only once (if they didn't change between runs).

Once you have installed the software, you can run the daemon with the `mvnd` command instead of the standard `mvn` one. Here are the results with `mvnd test`:

* 33.124 s (Wall Clock)
* 33.114 s (Wall Clock)
* 34.440 s (Wall Clock)
* 32.025 s (Wall Clock)
* 29.364 s (Wall Clock)

Note that the daemon uses multiple threads by default, with `number of cores - 1`.

## Mixing and matching

We've seen several ways to speed up the build. What if we used them in conjunction?

Let's first try with every technique we've seen so far in the same run:

```bash
mvnd test -Dparallel=all -DperCoreThreadCount=false -DthreadCount=16 -o #1 #2 #3 #4
```

1. Use the Maven daemon
2. Run the tests in parallel
3. Don't update `SNAPSHOT` dependencies
4. Configure the JVM parameters as above via the `jvm.config` file - no need to set any option

The command returns the following results:

* 27.061 s (Wall Clock)
* 24.457 s (Wall Clock)
* 24.853 s (Wall Clock)
* 25.772 s (Wall Clock)

Thinking about it, the Maven daemon *is* a long-running process. For that reason, it stands to reason to let the JVM analyze and compile the _bytecode_ to _native code_. We can thus remove the `jvm.config` file and re-run the above command. Results are:

* 23.840 s (Wall Clock)
* 26.589 s (Wall Clock)
* 22.283 s (Wall Clock)
* 23.788 s (Wall Clock)
* 22.456 s (Wall Clock)

Now we can display the consolidated results:

<table>
  <thead>
    <tr>
      <th>
      <th>Baseline
      <th>Parallel Build
      <th>Parallel tests
      <th>Offline
      <th>JVM params
      <th>Daemon
      <th>Daemon + offline + parallel tests + parameters
      <th>Daemon + offline + parallel tests
  <tbody>
    <tr>
      <th>#1 (s)
      <td>120.00
      <td>51.00
      <td>128.00
      <td>106.00
      <td>104.00
      <td>33.12
      <td>27.06
      <td>23.84
    <tr>
      <th>#2 (s)
      <td>117.00
      <td>40.00
      <td>123.00
      <td>106.00
      <td>104.00
      <td>33.11
      <td>24.46
      <td>26.59
    <tr>
      <th>#3 (s)
      <td>118.00
      <td>52.00
      <td>106.00
      <td>107.00
      <td>113.00
      <td>34.44
      <td>24.85
      <td>22.28
    <tr>
      <th>#4 (s)
      <td>116.00
      <td>42.00
      <td>112.00
      <td>115.00
      <td>113.00
      <td>32.03
      <td>25.77
      <td>23.79
    <tr>
      <th>#5 (s)
      <td>118.00
      <td>42.00
      <td>113.00
      <td>104.00
      <td>115.00
      <td>
      <td>29.36
      <td>22.46
    <tr>
      <th>Average (s)
      <td>*117.80*
      <td>45.40
      <td>116.40
      <td>107.60
      <td>109.80
      <td>32.41
      <td>25.54
      <td>23.79
    <tr>
      <th>Deviation
      <td>1.76
      <td>25.44
      <td>63.44
      <td>14.64
      <td>22.96
      <td>2.91
      <td>1.00
      <td>2.38
    <tr>
      <th>Gain from baseline (s)
      <td>-
      <td>72.40
      <td>1.40
      <td>10.20
      <td>8.00
      <td>85.39
      <td>92.26
      <td><strong>94.01</strong>
    <tr>
      <th>% gain
      <td>-
      <td>61.46%
      <td>1.19%
      <td>8.66%
      <td>6.79%
      <td>72.48%
      <td>78.32%
      <td><strong>79.80%</strong>
  </tbody>
</table>

## Conclusion

In this post, we have seen several ways to speed up your Maven build. Here's the summary:

* **Maven daemon**: A solid, safe starting point
* **Parallelize builds**: When the build contains multiple modules that are independent of each other
* **Parallelize tests**: When the project contains multiple tests
* **Offline**: When the project contains `SNAPSHOT` dependencies *and* you don't need to update them
* **JVM parameters**: When you want to go the extra mile

I'd advise every user to start using the Maven daemon and continue optimizing if necessary and depending on your project.

In the next post, we will focus on speeding your Maven builds in-container.

**To go further:**

* [Parallel builds in Maven 3](https://cwiki.apache.org/confluence/display/MAVEN/Parallel+builds+in+Maven+3)
* [Surefire Parallel Test Execution](https://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html#Parallel_Test_Execution)
* [mvnd - the Maven Daemon](https://github.com/mvndaemon/mvnd)

_Originally published at [A Java Geek](https://blog.frankel.ch/faster-maven-builds/1/) on October 3<sup>rd</sup>, 2021_