Maven

my life is short!

 

 

Peter Palaga

@ppalaga

Peter Palaga

 

↘     ↙

Camel Quarkus

Projects I build often

 

Lines of Java Code1 Maven modules2 mvn clean install
-DskipTests
Camel Quarkus 93K 1371 23.5 min
Camel 1609K 612 32.6 min
Quarkus 594K 1161 11.1 min

 

1) cloc --include-lang=Java .
2) mvn clean && find . -type f -name 'pom.xml' | wc -l

Agenda

  • Maven lifecycle
  • Skipping mojos
  • mvnd a.k.a. Maven Daemon
  • Vertical scaling
  • Incrememental builds with GIB

Maven lifecycle

-DskipTests

Why to skip mojos?

  • After pulling from team git repository
  • Source tree in a clean state
  • Checked by the team CI
  • Tests, and various source checks can be skipped

Find candidates for skipping

async-profiler or any other Java profiler

  • export MAVEN_OPTS="-agentpath:/path/to/libasyncProfiler.so=start, event=cpu,file=mvn-profile.html"
  • Outputs a flame graph
  • Summed mojo execution times are easy to see
mvn clean install -DskipTests -Dformatter.skip -Dimpsort.skip ...
# in Camel Quarkus

All possible skips for this project

$ mvn clean install \
    -DskipTests \
    -Dformatter.skip \
    -Dimpsort.skip \
    -Denforcer.skip \
    -Dcamel-quarkus.update-extension-doc-page.skip \
    -Dskip.installyarn \
    -Dskip.yarn \
    -Dlicense.skip \
    -Dquarkus.build.skip
    -Dgmaven.execute.skip # underway https://github.com/groovy/gmaven/pull/27
...
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  04:34 min  # down from 23:27 min by mvn clean install -DskipTests
    
Can we do better?
public class SomeMojo extends AbstractMojo {

    @Parameter(property = "some.skip", defaultValue = "false")
    boolean skip;

    @Override
    public void execute() {
        if (skip) {
            getLog().info("Skipping as requested by the user");
            return;
        }
        ...
    }
}
← What happens before?
  • Resolve and download plugin dependencies
  • Setup a classloader, load the necessary classes
  • Instantiate the Mojo
  • Inject fields (potentially expensive objects)
  • Call Mojo.execute()
How can we avoid all of this?

Remove plugins
from Maven execution plan
altogether

<project>
    ...                      <!-- Proper skipping -->
    <build>
        <plugins>
            <!-- Only plugins essential for the fast build -->
            <plugin><artifactId>maven-compile-plugin</artifactId></plugin>
            <plugin><artifactId>maven-jar-plugin</artifactId></plugin>
            <plugin><artifactId>maven-install-plugin</artifactId></plugin>
            ...
        </plugins>
    </build>
    <profiles>
        <profile>
            <id>full</id>
            <activation>
                <property>
                    <name>!quickly</name><!-- Active by default unless -Dquickly is passed -->
                </property>
            </activation>
            <build>
                <plugins>
                    <!-- All other plugins -->
                    <plugin><artifactId>maven-enforcer-plugin</artifactId></plugin>
                    <plugin><artifactId>maven-surefire-plugin</artifactId></plugin>
                    <plugin><artifactId>maven-failsafe-plugin</artifactId></plugin>
                    <plugin><artifactId>groovy-maven-plugin</artifactId></plugin>
                    ...
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

 

$ mvn clean install -Dquickly
...
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  04:01 min  # down from 04:34 min by
                               #
                               #   mvn clean install -DskipTests -Dformatter.skip ...
                               #

 

Can we do better?

Java is fast *

 

 

 

* When warmed up

Java Warmup costs

  • JVM boot
  • JIT (Just in Time) compilation

Cost of starting Maven

Measurable through a simple experiment:

package org.apache.maven;
public class DefaultMaven implements Maven {
    ...
    private MavenExecutionResult doExecute( MavenExecutionRequest request ) {
        System.out.println("End of Maven init: " + System.currentTimeMillis());
        request.setStartTime( new Date() ); // Maven measures from here
    }
    ...
}
 
$ echo $(($(date +%s%N)/1000000)) && mvn clean install -Dquickly
1634117024986
...
End of Maven init: 1634117025765
$ echo "$((1634117025765-1634117024986))"
779 # Maven start time in milliseconds

Cost of JIT compilation

mvn clean install -DskipTests -Dformatter.skip -Dimpsort.skip ...
# in Camel Quarkus

Eliminate JVM warm up

  • Have a long living Java process, a.k.a. daemon
  • Pass build requests through a socket
  • Like Gradle daemon, but for Maven

- the Maven Daemon

https://github.com/apache/maven-mvnd

  • Started by Guillaume Nodet in 2019
  • 26 releases since then
  • Donated to the ASF Maven Project in December 2021

Twitter

mvnd - how to install

Maven Daemon overview

mvnd - the client

  • GraalVM native executable
  • Looks up a running daemon
    • Or starts a new one
  • Sends a build request via socket
  • Receives events from the daemon
  • Displays the progress

⇄       

Daemon

  • Long running Java process
  • Embeds a specific Maven version
    • Does not use any local Maven installation
  • Receives build requests
  • Caches plugin class loaders
  • Exits after a configurable period of time

mvnd demo

$ git clone camel quarkus
$ cd camel-quarkus
$ mvnd install -Dquickly
...
# Try CTRL B, + and - keystrokes
# Check mvnd --status and mvnd --help in another terminal
$ mvnd install -Dquickly
...
$ mvnd install -Dquickly
...

mvnd speed gains

A build with Many modules
(relatively independent)
cd camel-quarkus && \
   mvn[d] clean install -Dquickly      
    Single module
 
cd camel-quarkus/extensions-core/core/deployment && \
   mvn[d] clean install \
   -DskipTests             -Dtest=CamelBeansUnremovableTest
mvn baseline       4:01 min 10.11 sec 12.79 sec
mvnd 1st 1:19 (3x) 9.15 (1.1x) 11.82 (1.1x)
mvnd 2nd 1:04 (3.8x) 2.37 (4.3x) 5.26 (2.4x)
mvnd 3rd 1:01 (4x) 1.19 (5.3x) 4.86 (2.6x)
Gains through
  • Class loader caching
    • No repeated JIT
    • Faster JIT-compiled code
  • Parallel execution
  • No JVM boot cost
  • Class loader caching

mvnd - Parallel builds

  • Default number of threads:    Runtime.getRuntime().availableProcessors() - 1
  • -T<n> or -T<n>C supported like with stock Maven

Maven builder

  • Pluggable builder since Maven 3.2.1
  • A strategy for scheduling and building modules
  • singlethreaded (default)
  • multithreaded (with -T)

mvnd - Smart builder

Issues of parallel builds

Hidden dependencies

  •     A
       / \     Maven module dependencies
      B   C    (Lower depends on upper)
       \ /
        D
    
    No issues with serial builder
  • Possible issues with a parallel builder:
    • C could be reading a file in B's target folder
    • C's test could dynamically read an artifact produced by B from the local Maven repository

Simple remedy: -1/--serial
to force the serial builder

Better remedy:
Make the dependency explicit

Make hidden dependencies explicit

                                    <!-- Add this in C -->
    org.my-group
    B
    ${project.version}
    pom
    test
    
        
            *
            *
        
    

This won't add any real dependency to C but it will guarantee that B is fully built before C

Issues of parallel builds

Broken plugins

Plugins may do nasty things

  • Mutable global state
  • Race conditions

-1/--serial may help

Better: Report/fix the issue in the given plugin

mvnd drawbacks

  • Blocks a few gigs of RAM (configurable)
  • First official build (0.8.2) for Apple M1 these days
  • Windows issues and testing catching up gradualy

Vertical scaling

Laptop vs. desktop

A build with Many modules (relatively independent)
cd camel-quarkus && mvn[d] clean install -Dquickly      
Machine Lenovo ThinkPad P1 Gen 3
Core i7 10850H
12 cores
2.7 GHz
(baseline)
    Self made desktop
AMD Ryzen Threadripper 1920X
12 cores, 24 threads
3.5 GHz (1.3x)
mvn       4:01 min 3:42 min (1.1x)
mvnd 1st 1:19 0:55 (1.4x)
mvnd 2nd 1:04 0:42 (1.5x)
mvnd 3rd 1:01 0:40 (1.5x)
Can we do better?

Costs laptop vs. desktop

Machine Lenovo ThinkPad P1 Gen 3
Core i7 10850H
12 cores
2.7 GHz
    Self made desktop
AMD Ryzen Threadripper 1920X
12 cores, 24 threads
3.5 GHz (1.3x)
Purchase price       2630,- €
(Oct. 2021)
~1300 €
(Feb. 2020, w/out 🖥️ ⌨️ 🖱️)
Power suply unit 135 W 650 W
Idle ~18 W* ~60 W*
Max 129 W* 278 W*
2h work session
(builds, web browsing)
0.06 kWh* 0.21 KWh*
€/year (0.64 €/kWh**) 49 € 171 €


*) Measured with a budget power meter from a hobby market
**) ENGIE single rate                                                                          

Wrap up

Techniques to speed up Maven builds

  • Skip unessential mojos
  • Maven daemon to keep the building JVM warm
  • More CPU cores & RAM