FreshMarker Performance (2)

“When I am working on a problem, I never think about beauty. I think only of how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong.”

R Buckminster Fuller

In the first article on FreshMarker performance, some of the template engine’s speed problems were addressed. The benchmark showed a tripling of the speed for these adjustments. In this article, the use of Reflections will be removed, which is certainly also of interest outside of a template engine.

Reflections is part of the Java API and makes it possible to determine and manipulate information about classes, methods, fields and constructors at runtime, even if they are not known at compile time. This allows developers to create instances of classes, call methods and change fields without knowing the name of the classes or their members at compile time. Reflections is often used in frameworks and libraries that need to provide flexible and generic functionality, such as FreshMarker.

In addition to the many possibilities that Reflections offers, there are also some significant disadvantages. One of the main disadvantages that concerns us here is the loss of performance, as Reflections is generally slower than direct code access, especially for frequent and complex operations.

Reflections is used within FreshMarker for Built-Ins and access to Java Beans and Records.

Built-ins are methods that can be called on instances of the model classes. There are built-ins for the TemplateString class such as uppercase and kebab-case, counter and item_parity for the TemplateSequenceLooper class and many more.

So far, there have been two approaches to defining Build-Ins. The use of Reflections and the use of Lambdas.

The use of Reflections is based on public static methods whose first parameter is the instance on which the Build-In operates. The return value is obviously the result of the Build-In. Additional parameters can be Built-In parameters of type TemplateObject or the ProcessContext.

The following are four simple Build-Ins for the TemplateSequenceLooper and TemplateHashLooper classes.

@BuiltInMethod
public static TemplateNumber counter(TemplateSequenceLooper value) {
  return value.getCounter();
}

@BuiltInMethod
public static TemplateNumber counter(TemplateHashLooper value) {
  return value.getCounter();
}

@BuiltInMethod
public static TemplateString itemParity(TemplateSequenceLooper value) {
  return (TemplateString) value.cycle(ITEM_PARITYTY);
}

@BuiltInMethod
public static TemplateString itemParity(TemplateHashLooper value) {
  return (TemplateString) value.cycle(ITEM_PARITYTY);
}

To ensure that the Built-In methods are recognized as such, they are annotated with @BuildInMethod. As all four Built-Ins do not require any further parameters, only one parameter of the type TemplateSequenceLooper or TemplateHashLooper is passed.

What is expensive about this solution is not only the invoke method on the Method instance for the respective method, but also the provision of the parameters.

public interface BuiltInFunction {
  TemplateObject apply(TemplateObject value, List<TemplateObject> parameters, ProcessContext context);
}

FreshMarker calls the apply method of BuiltInFunction for a Built-In call. The parameter array for the invoke method must then first be provided from its parameters. This is an inexpensive operation for the example Built-Ins without parameters, but not insignificant if there are several parameters.

To remove the Reflections approach, all you have to do is convert all Built-In methods into Lambdas.

builtIns.put(HASH.of("counter"), (x, y, e) -> ((TemplateHashLooper) x).getCounter());
builtIns.put(SEQUENCE.of("counter"), (x, y, e) -> ((TemplateSequenceLooper) x).getCounter());
builtIns.put(HASH.of("item_parity"), (x, y, e) -> ((TemplateHashLooper) x).cycle(ITEM_PARITYTY));
builtIns.put(SEQUENCE.of("item_parity"), (x, y, e) -> ((TemplateSequenceLooper) x).cycle(ITEM_PARITYTY));

The Map builtIns contains instances of the type BuiltInFunction as values. These can, like these four here, be written as lambda expressions. With a little effort, all Built-In methods could be rewritten as Lambdas and the call to Built-Ins no longer contains any Reflections.

The solution for accessing Java Beans and Records without using Reflections also has something to do with Lambdas, but not as simple as with Built-Ins.

Java Beans and Records are interpreted as a Hash in FreshMarker. A Hash is a data structure that maps names to values. For Java developers, this means a Map with String instances as keys.

FreshMarker‘s data model does not care whether it is a Java Bean, a Record or a Map. All three are accessed via the Map interface in the TeamplateBean class. A BaseReflectionsMap instance is created for Java Beans and Records, which controls access to the getter methods.

public class TemplateBeanProvider {

  private final Map<Class<?>, Map<String, Method>> methodBeans = new HashMap<>();

  public Map<String, Object> provide(Object bean, BaseEnvironment environment) {
    final Map<String, Method> methods = methodBeans.computeIfAbsent(bean.getClass(), b -> collectMethods(bean));
    return new BaseReflectionsMap(methods, environment, bean);
  }

  private Map<String, Method> collectMethods(Object bean) {
    try {
      BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass(), Object.class);
      return Stream.of(beanInfo.getPropertyDescriptors()).collect(toMap(FeatureDescriptor::getName, PropertyDescriptor::getReadMethod));
    } catch (IntrospectionException e) {
      throw new ProcessException(e.getMessage(), e);
    }
  }
}

The TemplateBeanProvider creates the Map for Java Beans by using the Introspector to read the getter methods of the Java Bean. A corresponding class also exists for the Records.

An alternative to the TemplateBeanProvider is shown here. Access to a getter method is realized via the Getter interface.

public interface Getter {
  Object get(Object instance);
}

For a Java Bean Example with the attribute value, there is access via the ExampleValueGetter.

public class ExampleValueGetter implements Getter {
    @Override
    public Object get(Object instance) {
        return ((Example)instance).getValue();
    }
}
Getter getter = new ExampleValueGetter();

Or as a shorter lambda expression.

Getter getter = x -> ((Example)x).getValue()

The value can then be read out with getter.get(new Example(42));.

However, no Lambdas can be created for Java Beans and Records to read the attributes of the classes in FreshMarker because the classes are not necessarily known at compile time. Fortunately, this is not correct. Since Java 8, lambdas can also be created at runtime.

The LambdaMetafactory is a special class that is used to create lambda expressions and method references at runtime. It makes it possible to create dynamically implemented functionalities that would otherwise have to be defined at compile time. This class uses the Method Handles API to enable more efficient and secure dynamic method calls.

Another provider that uses the LambdaMetafactory is shown below.

public class TemplateMapGetterProvider {
  private final Function<Class<?>, Map<String, Method>> methodSupplier;

  public TemplateMapGetterProvider(Function<Class<?>, Map<String, Method>> methodSupplier) {
    this.methodSupplier = methodSupplier;
  }

  public interface Getter {
    Object get(Object instance);
  }

  private final Map<Class<?>, Map<String, Getter>> methodBeans = new HashMap<>();

  public Map<String, Object> provide(Object bean, BaseEnvironment environment) {
    final Map<String, Getter> methods = methodBeans.computeIfAbsent(bean.getClass(), b -> collectGetters(bean));
    return new BaseGetterMap(methods, environment, bean);
  }

  private Map<String, Getter> collectGetters(Object bean) {
    final MethodHandles.Lookup lookup = MethodHandles.lookup();
    Map<String, Getter> result = new HashMap<>();
    for (Entry<String, Method> entry : methodSupplier.apply(bean.getClass()).entrySet()) {
      result.put(entry.getKey(), wrapGetter(lookup, entry.getValue()));
    }
    return result;
  }

  private static Getter wrapGetter(Lookup lookup, Method method) {
    try {
      MethodHandle handle = lookup.unreflect(method);
      return (Getter) LambdaMetafactory.metafactory(lookup, "get",
          MethodType.methodType(Getter.class), MethodType.methodType(Object.class, Object.class),
          handle, handle.type()).getTarget().invoke();
    } catch (Throwable e) {
      throw new IllegalArgumentException("Could not generate the function to access the getter " + method.getName(), e);
    }
  }
}

In the collectGetters method, all getter methods are first read out with the Introspector and then a Getter instance is created for each getter method in the wrapGetter method. The Getter instances are then used in the BaseGetterMap class to access the attributes of the Java Bean.

This means that access to the attributes of a Java Bean via Reflections has been replaced by faster access via Lambdas. The benchmark also confirms this, as more than 15000 pages per second are now generated.

Benchmark               Mode  Cnt      Score    Error  Units
FreshMarker.benchmark  thrpt   50  15398,787 ± 61,881  ops/s

The new version of the library can be found on Maven Central. Have fun trying it out.

Leave a Comment