Parallel Ant
This article explains how you can expect to speed up Java builds using threading with Apache Ant.
Disclaimer
This is not a magic way to make your builds run faster. As I will explain, there is very little time to win. If you think that you waste too much time waiting for your Ant build to complete, first ensure that it is incremental.
Problem
How a Java build works
Building a program written in the C programming language is usually a two steps operation:
- generation of the object files (*.o) from the source files (*.c),
- linkage of the object files into the executable.
In order to speed up builds, build systems often provide an option to run several jobs in parallel, what is possible within step #1 and helps you save a lot of time, especially if you have a multi-core CPU. For example, you can do this with Make with the -j option: several *.c files will be compiled into an object file at the same time.
In Java, if you ask the compiler (javac) to compile a single Java class MyClass, it will actually build this class and every other class in your project which is referenced in MyClass (transitively). This means that you have no control over parallelism as you have with C. See how javac searches for types for more details.
Moreover, javac is currently a single-threaded process, although this could change in the future.
In short
To sum up, if you want to build a single jar from a set of .java files, you cannot expect anything to be performed in parallel:
- compilation is a one step operation and is single-threaded,
- you cannot start packing the jar until all .class files are generated.
However, there may be opportunity for using parallelism in complex builds. For example, you could:
- build two non-dependent modules at the same time,
- publish your jars to a local repository and a remote one at the same time,
- etc.
This is what the second part of this article will focus on.
Parallelizing Ant builds
Checking out the code
The ability of running tasks in several threads is given by a little plugin I wrote for Ant available in a public Subversion repository. To check out the code and build the project:
% svn co http://svn.jpountz.net/pant/trunk pant-trunk % cd pant-trunk % ant
Note: You need Java 5 or higher to be able to compile this project.
pant-${version}.jar will be written under the build directory.
Notion of dependencies between targets
In Ant build files, you can define dependencies between targets. A target T1 depends on another target T2 if T1 relies on T2's output to run. Let's imagine the following dependency graph:
Note: Dependency graphs were generated using Stefan Kost's XSL file, xsltproc and Graphviz.
This dependency graph has been computed from the following build file:
<?xml version="1.0"?>
<project name="test" default="master" basedir=".">
<target name="init">
<echo message="Init started" />
<sleep seconds="1" />
<echo message="Init done" />
</target>
<target name="init1" depends="init">
<echo message="Init1 started" />
<sleep seconds="1" />
<echo message="Init1 done" />
</target>
<target name="init4" depends="init1, init3">
<echo message="Init4 started" />
<sleep seconds="1" />
<echo message="Init4 done" />
</target>
<target name="init2" depends="init">
<echo message="Init2 started" />
<sleep seconds="1" />
<echo message="Init2 done" />
</target>
<target name="init3">
<echo message="Init3 started" />
<sleep seconds="2" />
<echo message="Init3 done" />
</target>
<target name="slave1" depends="init4">
<echo message="Slave1 starting..." />
<sleep seconds="2"/>
<echo message="Slave1 done." />
</target>
<target name="slave2" depends="init2">
<echo message="Slave2 starting..." />
<sleep seconds="2"/>
<echo message="Slave2 done." />
</target>
<target name="master" depends="slave1, slave2">
<echo message="Master starting..." />
<sleep seconds="1" />
<echo message="Master done." />
</target>
</project>
This is a purely virtual use case, but you can easily imagine how this kind of build could be run in several threads. If it doesn't matter to run slave1 before or after slave2, then they could probably be executed in parallel.
Attribution of tasks to threads
Running targets in several threads raises two issues. Let's imagine that T2 depends (directly or transitively) on T1:
- if T1 and T2 are assigned to the same thread then T2 should be executed before T1 in order not to deadlock execution,
- if T1 and T2 are assigned to two different threads then T1 will have to wait for T2 to finish.
To solve theses issues:
- targets are first sorted in an order which satisfies the dependency graph (if T1 depends on T2 then T2 < T1),
- targets cannot start before they received a signal from each dependency saying they finished.
Logging
Ant's default logger assumes that the build is sequential, because for any target it first prints the name of a target and then the logs corresponding to the nested tasks. As a consequence, if two targets are running in parallel, you cannot know the target owner of the task log. In order to solve this problem, I changed Ant's logging format from:
target:
[task1] log1
[task2] log2
to
+ target
[target / task1] log1
[target / task2] log2
- target
+ target means that the target started executing whereas - target means that the target finished. Since this logging format is not contextual, there is no issue with using it for parallel targets.
In action
Let's try to run the previous build file.
Ant
jpountz@zreptik ~% ant
Buildfile: build.xml
init:
[echo] Init started
[echo] Init done
init1:
[echo] Init 1 started
[echo] Init 1 done
init3:
[echo] Init 3 started
[echo] Init 3 done
init4:
[echo] Init 4 started
[echo] Init 4 done
slave1:
[echo] Slave 1 starting...
[echo] Slave 1 done.
init2:
[echo] Init 2 started
[echo] Init 2 done
slave2:
[echo] Slave 2 starting...
[echo] Slave 2 done.
master:
[echo] Master starting...
[echo] Master done.
BUILD SUCCESSFUL
Total time: 11 seconds
Parallel Ant with one thread
jpountz@zreptik ~% ant -lib pant-0.1.jar \
-Dant.executor.class=net.jpountz.ant.helper.ParallelExecutor \
-Dant.executor.threadcount=1 \
-logger net.jpountz.ant.helper.ParallelExecutorLogger
Buildfile: build.xml
Running tasks in 1 thread
+ init3
[init3 / echo] Init 3 started
[init3 / echo] Init 3 done
- init3
+ init
[init / echo] Init started
[init / echo] Init done
- init
+ init1
[init1 / echo] Init 1 started
[init1 / echo] Init 1 done
- init1
+ init2
[init2 / echo] Init 2 started
[init2 / echo] Init 2 done
- init2
+ slave2
[slave2 / echo] Slave 2 starting...
[slave2 / echo] Slave 2 done.
- slave2
+ init4
[init4 / echo] Init 4 started
[init4 / echo] Init 4 done
- init4
+ slave1
[slave1 / echo] Slave 1 starting...
[slave1 / echo] Slave 1 done.
- slave1
+ master
[master / echo] Master starting...
[master / echo] Master done.
- master
BUILD SUCCESSFUL
Total time: 11 seconds
Parallel Ant with three threads
jpountz@zreptik ~% ant -lib pant-0.1.jar \
-Dant.executor.class=net.jpountz.ant.helper.ParallelExecutor \
-Dant.executor.threadcount=3 \
-logger net.jpountz.ant.helper.ParallelExecutorLogger
Buildfile: build.xml
Running tasks in 3 threads
+ init3
+ init
[init / echo] Init started
[init3 / echo] Init 3 started
[init / echo] Init done
- init
+ init2
[init2 / echo] Init 2 started
+ init1
[init1 / echo] Init 1 started
[init2 / echo] Init 2 done
- init2
+ slave2
[slave2 / echo] Slave 2 starting...
[init1 / echo] Init 1 done
- init1
[init3 / echo] Init 3 done
- init3
+ init4
[init4 / echo] Init 4 started
[init4 / echo] Init 4 done
- init4
+ slave1
[slave1 / echo] Slave 1 starting...
[slave2 / echo] Slave 2 done.
- slave2
[slave1 / echo] Slave 1 done.
- slave1
+ master
[master / echo] Master starting...
[master / echo] Master done.
- master
BUILD SUCCESSFUL
Total time: 6 seconds
As you can see, the build took only 6 seconds. Let's see what targets executed concurrently:
A real world example
The previous example was too easy. Let's now try with a real world example: the building of Ant itself. Here is the graph of dependencies between targets. The default target is main (top right of the schema). Is there any place for concurrent execution of targets?
Let's build Ant with five threads.
jpountz@zreptik ~% ant -lib pant-0.1.jar \
-Dant.executor.class=net.jpountz.ant.helper.ParallelExecutor \
-Dant.executor.threadcount=5 \
-logger net.jpountz.ant.helper.ParallelExecutorLogger
Buildfile: build.xml
Running tasks in 5 threads
+ prepare
+ check_for_optional_packages
- prepare
- check_for_optional_packages
+ build
[build / mkdir] Created dir: /home/jpountz/src/ant/build
[build / mkdir] Created dir: /home/jpountz/src/ant/build/classes
[build / mkdir] Created dir: /home/jpountz/src/ant/build/lib
[build / javac] Compiling 750 source files to /home/jpountz/src/ant/build/classes
[build / javac] Note: Some input files use or override a deprecated API.
[build / javac] Note: Recompile with -Xlint:deprecation for details.
[build / copy] Copying 7 files to /home/jpountz/src/ant/build/classes
[build / copy] Copying 2 files to /home/jpountz/src/ant/build/classes
[build / copy] Copying 2 files to /home/jpountz/src/ant/build/classes/org/apache/tools/ant/taskdefs/optional/junit/xsl
- build
+ compile-tests
[compile-tests / mkdir] Created dir: /home/jpountz/src/ant/build/testcases
+ jars
[jars / copy] Copying 2 files to /home/jpountz/src/ant/build
[jars / copy] Copying 1 file to /home/jpountz/src/ant/build
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-launcher.jar
[compile-tests / javac] Compiling 278 source files to /home/jpountz/src/ant/build/testcases
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant.jar
[compile-tests / javac] Note: Some input files use or override a deprecated API.
[compile-tests / javac] Note: Recompile with -Xlint:deprecation for details.
[compile-tests / jar] Building jar: /home/jpountz/src/ant/build/testcases/org/apache/tools/ant/taskdefs/test2-antlib.jar
- compile-tests
+ test-jar
[test-jar / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-testutil.jar
- test-jar
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-bootstrap.jar
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-nodeps.jar
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-trax.jar
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-resolver.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-resolver.jar because no files were included.
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-junit.jar
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-regexp.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-regexp.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-oro.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-oro.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-bcel.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-bcel.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-log4j.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-log4j.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-commons-logging.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-commons-logging.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-bsf.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-bsf.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-stylebook.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-stylebook.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-javamail.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-javamail.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-netrexx.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-netrexx.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-commons-net.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-commons-net.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-antlr.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-antlr.jar because no files were included.
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-jmf.jar
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-jai.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-jai.jar because no files were included.
[jars / jar] Building jar: /home/jpountz/src/ant/build/lib/ant-swing.jar
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-jsch.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-jsch.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-jdepend.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-jdepend.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-xalan2.jar because no files were included.
[jars / jar] Warning: skipping jar archive /home/jpountz/src/ant/build/lib/ant-apache-xalan2.jar because no files were included.
- jars
+ dist-lite
[dist-lite / mkdir] Created dir: /home/jpountz/src/ant/dist
[dist-lite / mkdir] Created dir: /home/jpountz/src/ant/dist/bin
[dist-lite / mkdir] Created dir: /home/jpountz/src/ant/dist/lib
[dist-lite / copy] Copying 8 files to /home/jpountz/src/ant/dist/lib
[dist-lite / copy] Copying 2 files to /home/jpountz/src/ant/dist/lib
[dist-lite / copy] Copying 13 files to /home/jpountz/src/ant/dist/bin
- dist-lite
+ main
- main
BUILD SUCCESSFUL
Total time: 14 seconds
Did some targets execute in parallel?
As you can see,
- check_for_optional_packages and prepare started executing simultaneously,
- compile-tests and jars started executing simultaneously,
- test-jar started executing before jars finished.
You can compare with the graph for one thread:
On my laptop, the parallel build took 600ms less than the sequential build on average. In my opinion, this is both good and bad, good because it is faster on average and bad because the actual bottleneck is disk access here, so parallelizing the build cannot make it run much faster.
Other implementations
Other people worked on parallel execution of targets with Apache Ant. You can find another plugin which does almost the same thing in Apache Ant's sandbox. Despite this implementation is based on the same principles, the way it actually works is different: My implementation decides at the beginning of the build in what order targets will be sent to the thread pool and targets wait for their dependencies to finish using a CountDownLatch. On the order hand, the sandbox implementation assigns a status to every target (not started, running, finished, failed, etc.), and iterates over targets which have not started yet every time a target has finished and asks them to try to run: either one of their dependencies has not finished and they don't do anything or their dependencies have finished and they execute.
Conclusion
Using threads to parallelize the execution of targets may help you build your projects slightly faster. I think that the bottleneck in most builds is the file sytem, but if you have projects for which this plugin reduces the build time significantly, I would be very happy to hear about it, you can send me an email at jpountz (at) dinauz (dot) org.