mandag 28. februar 2011

Incremental builds - great potential, reliant on trust

The first time I evaluated gradle as a candidate build system I was intrigued by its incremental build feature. Having worked with multiprojects in maven previously I must admit I was pretty hardwired into using "mvn clean install". With maven that felt as the only safe bet way to ensure that any changes in subprojects didn't break the overall build. However for large multiproject builds it sure felt as I was spending a lot of time waiting for a range of build activities that shouldn't have been necessary.
What if my build system could be clever enough to do just the activities it must, skip the others and still ensure the overall build integrity ? Gradle came with a promise to go a long way doing just that, so obviously I had to try it out.


Sample multiproject:

  • dumpling : Root multiproject (packaged as pom in maven )
  • dumpling-util : A utility project (jar)
  • dumpling-core : Application domain (jar, depends on dumpling-util)
  • dumpling-webapp: Web app  (depends on dumbling-core, and dumpling-util transitively)


To see how gradle and maven behaved I performed some simple trials. For each step I observed the main activities that was reported.


Sample scenario 1: Non breaking change

Precondition:

  • maven: mvn clean install
  • gradle: gradle clean build

Changed a source file in dumpling-util. The change is internal to the class. Then for maven and gradle I did following:

  • maven: mvn install
  • gradle: gradle build



dumpling-util observations:
Taskmvn observationgradle observation
resources copied resources (not needed) skipped
compile compiled changed file compiled changed file
test resources copied resources skipped
test compile no work compiled test sources
jar created created
install installed to local repo n/a



dumpling-core observations:
Task
mvn observation
gradle observation
resources
copied resources (not needed)
skipped
compile
none
full recompile (due to input file dumpling-core jar had changed)
test resources
copied resources
skipped
test compile
no work
full recompile (due to input file dumpling-core jar had changed)
jar
created
skipped (input classfiles and resources same checksum as previous run)
install
installed to local repo
n/a



dumpling-webapp observations:
Task
mvn observation
gradle observation
resources
copied resources (not needed)
skipped
compile
none
full recompile (due to transitive input file dumpling-core jar had changed)
test resources
copied resources
skipped
test compile
no work
full recompile (due to transitive input file dumpling-core jar had changed)
war
created
created (due to transitive input file dumpling-core jar had changed)
install
installed to local repo
n/a

Comparing the two one might at first conclude that both did unnecessary work. 

Maven: Copied resources and test resources for all subprojects eventhough none of them had changed. For dumpling-core it created the jar eventhough not a single file in the jar had changed.
For dumpling-core and dumpling-webapp it didn't do any work compiling java sources ar test sources. This might seem ok, but next scenario will prove this to be untrue.

Gradle: It seems unnecessary to do some of the activities that gradle does, but there is a reasonable explanation behind. In gradle there is a concept of inputs and outputs for tasks. A task may have a defined set of inputs (files) and a defined set of outputs (files). For each execution of a task gradle calculates checksums for the inputs and the outputs. If gradle finds that the checksums have changed since last run, the tasks will be run (unless other factors such as explicit disabling of tasks has been configured). So an example would be dumpling-util:test compile. This task is executed because its input files from dumpling-util:compile has changed.

Sample scenario 2: Breaking change

I changed the interface of a method for a class in dumpling-util, and updated its test case accordingly. The method in question is used by a class in dumpling-core, so dumpling-core needs to change...
Then for maven and gradle I did following:
  • maven: mvn install
  • gradle: gradle build

dumpling-util observations:
Task
mvn observation
gradle observation
resources
copied resources (not needed)
skipped
compile
compiled changed file
compiled changed file
test resources
copied resources
skipped
test compile
compiled changed test source file
compiled test sources
jar
created
created
install
installed to local repo
n/a


dumpling-core observations:
Task
mvn observation
gradle observation
resources
copied resources (not needed)
skipped
compile
none
full recompile (due to input file dumpling-core jar had changed). 

BUILD Failed due to compiler error on class in dumpling-core that uses changed interface method of class from dumpling-util
test resources
copied resources
n/a
test compile
no work
n/a
jar
created
n/a
install
installed to local repo
n/a


dumpling-webapp observations:
Task
mvn observation
gradle observation
resources
copied resources (not needed)
n/a
compile
none
n/a
test resources
copied resources
n/a
test compile
no work
n/a
war
created
n/a
install
installed to local repo
n/a


It instantly becomes obvious that the incremental build features of maven is rather limited. Gradle on the other hand fails fast where it should. Obviously maven has never advertised great support for incremental builds, but the comparison helps put things into perspective.

Parting words
The potential of huge savings in time due to incremental builds is obvious. However vitally important for the feature is the level of trust you can put into it as well. In gradle you have great flexibility of creating your own tasks, if these rely on file inputs and outputs be sure to consider using the inputs/outputs mechanisms available to you. But do take care  when defining them, otherwise you might end up with a trust issue and reverting back to "clean build" :-)




2 kommentarer:

  1. Which Gradle version and which Maven version have you used for this comparison?

    SvarSlett
  2. gradle 0.9 rc3, maven 2.2.1 (and partially maven 3.0.2 using maven-shell 1.0.1)

    SvarSlett