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:

  1. generation of the object files (*.o) from the source files (*.c),
  2. 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:

However, there may be opportunity for using parallelism in complex builds. For example, you could:

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:

Build dependencies

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:

To solve theses issues:

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:

Build concurrency

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?

Ant build dependency graph

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?

Concurrency of targets

As you can see,

You can compare with the graph for one thread:

Concurrency of targets

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.