Igor Kromin |   Consultant. Coder. Blogger. Tinkerer. Gamer.

Ah Apache Velocity is something that I've always liked and we tend to make moderate use of it on a project I'm working on. So it was an off-chance that I happened to be looking at some code that was dealing with Velocity configuration that I noticed that custom directives are used all over the place instead of simple utility (or tool) classes. This wasn't a part of the code base that I was familiar with so thought there must be a reason for this choice.

Note: this article is based on Velocity 1.7 because that is the version in use. I know version 2.0 exists and it probably has a very different performance profile compared to what I have shown here. This exercise was more of an outside of work curiosity and not a full assessment of Velocity's performance.

Directives are harder to code and aren't very well documented. On the other hand utility/tool classes are documented and are an integral part in how Velocity is designed to be extended. So in my mind, going to the trouble of writing custom directives has only one purpose - performance gains.

So I set up a very simple test harness that would call merge() the same template 100 times. In one of the cases I made use of a utility/tool class and the other case used a custom directive. The only functionality that both of these had was to take an input string and output it as is.

This was the code for the harness (with the run*() calls commented out). Note that I made sure to start timing only around the merge() method, excluding VelocityContext population, though in reality this made little difference.
 Java
public class VelocityTest {
public static final void main(String[] args) {
VelocityEngine engine = new VelocityEngine();
engine.addProperty(RuntimeConstants.RESOURCE_LOADER, "class");
engine.addProperty("class.resource.loader.class",
ClasspathResourceLoader.class.getName());
engine.addProperty("class.resource.loader.cache", "true");
engine.addProperty("class.resource.loader.modificationCheckInterval", "0");
engine.loadDirective(MyOpDirective.class.getName());
for (int i = 0; i < 100; i++) {
/*runUtility(engine);*/
/*runDirective(engine);*/
}
}
private static void runUtility(VelocityEngine engine) {
StringWriter writer = new StringWriter();
Template template = engine.getTemplate("template1.vm");
VelocityContext context = new VelocityContext();
context.put("myOp", new MyOp());
context.put("var1", "Hello World");
long startTime = System.currentTimeMillis();
template.merge(context, writer);
System.out.println(System.currentTimeMillis() - startTime);
}
private static void runDirective(VelocityEngine engine) {
StringWriter writer = new StringWriter();
Template template = engine.getTemplate("template2.vm");
VelocityContext context = new VelocityContext();
context.put("var1", "Hello World");
long startTime = System.currentTimeMillis();
template.merge(context, writer);
System.out.println(System.currentTimeMillis() - startTime);
}
}


The utility class and its corresponding template looked like this. It doesn't get any simpler...
 Java
public class MyOp {
public String doSomething(String inpStr) {
return inpStr;
}
}

 Template 1 - Utility Class
#foreach ($number in [1..100])
$myOp.doSomething($var1)
#end




The directive was more complicated, but it did essentially the same thing as the utility class. Because this was an inline directive it also output a new line after the input string so that I could get the same strings out of the merged template.
 Java
public class MyOpDirective extends Directive {
@Override
public String getName() {
return "myOp";
}
@Override
public int getType() {
return LINE;
}
@Override
public boolean render(InternalContextAdapter context, Writer writer, Node node)
throws IOException, ResourceNotFoundException,
ParseErrorException, MethodInvocationException
{
if (node.jjtGetChild(0) != null) {
String inpStr = String.valueOf(node.jjtGetChild(0).value(context));
writer.write(inpStr);
writer.write("\n");
}
return true;
}
}

 Template 2 - Directive Class
#foreach ($number in [1..100])
#myOp($var1)
#end


I had to modify both of the templates to change their foreach loops to increase the number of iterations since my initial count of 100 didn't show any significant figures i.e. it was taking something like 0-1 ms to run. These tests were done on my 2017 MacBook Pro. The minimum number of iterations that showed significant results was 10,000. I also then added 100,000 and 1,000,000 to see how it scaled.

The results were interesting...
velperf_6.png

velperf_2.png velperf_3.png velperf_1.png



To my surprise, the custom directive was always faster than a utility class! Between 15-20% faster in fact. My expectation was that it would be the other way around. The initial few merge() calls showed even higher variations - setup time for the custom directive seemed to be much lower than for a utility class.

So my results show that directives appear to be much faster to use when compared to a utility/tool class. However these results are hardly realistic since templates will not have so many invocations of the same directive/utility over and over. Still there's something interesting here and maybe this was the reason for writing this code in the first place by whoever wrote it!

If it were up to me I would go with the utility/tool class approach because of its ease of implementation and maintainability. Using a utility/tool also decouples your code from Velocity internals which is very much what the directive approach depends on. Without a compelling reason to optimise, writing clean, maintainable code should always take precedence.

-i

Hope you found this post useful...

...so please read on! I love writing articles that provide beneficial information, tips and examples to my readers. All information on my blog is provided free of charge and I encourage you to share it as you wish. There is a small favour I ask in return however - engage in comments below, provide feedback, and if you see mistakes let me know.

If you want to show additional support and help me pay for web hosting and domain name registration, donations, no matter how small, are always welcome!

Use of any information contained in this blog post/article is subject to this disclaimer.
comments powered by Disqus
Other posts you may like...