The PooledTemplateProcessor

“An ounce of performance is worth pounds of promises.”

Mae West

Memory management is a major challenge when it comes to improving the performance of Java applications. When many objects are created, memory must be allocated for them; these objects must be managed and eventually deleted, and their memory released. This comes at a cost, and an application that consistently works with fresh objects will suffer from reduced performance.

With Freshmarker 2.7.0, the PooledTemplateProcessor has been added, which guarantees a performance boost in template processing; however, this comes at the cost of compromises elsewhere.

Template template = builder.getTemplate("example", "Hello ${name}!");
String result1 = template.process(Map.of("name", "Alice"));
String result2 = template.process(Map.of("name", "Bob")); 

In the example above, a template is created and called twice in succession with a different model, resulting in two different outputs. In order for processing to begin in the process method, various data structures must be created. These are created not only when the first process method is called, but also when the second is called. The reason for this is simple: the data structures represent the state of the respective template processing. If templates in different threads share this state, errors are inevitable.

On the other hand, however, this means that template processing operations in the same thread can reuse the data structures from the previous processing. That is the fundamental idea behind the PooledTemplateProcessor. It performs the template processing itself and always uses the same data structures.

Template template = builder.getTemplate("example", "Hello ${name}!");
PooledTemplateProcessor processor = PooledTemplateProcessor.of(template);
String result1 = processor.process(Map.of("name", "Alice"));
String result2 = processor.process(Map.of("name", "Bob")); 

The implementation of the PooledTemplateProcessor presented here represents the current state of development for the planned version 3.0.0. It is somewhat cleaner than the experimental version 2.7.0. The PooledTemplateProcessor is created using a Template in the PooledTemplateProcessor.of method. Afterward, the process method can be called on the PooledTemplateProcessor instead of on the template. Since the PooledTemplateProcessor is not thread-safe, it is up to the user to ensure that multiple threads do not access the PooledTemplateProcessor instance simultaneously.

The PooledTemplateProcessor is not created directly in the method; this is delegated to the createProcessor method in the Template class. To prevent people from messing around too much with the method, it is package-protected and not public.

public static PooledTemplateProcessor of(Template template) {
  return template.createProcessor(PooledTemplateProcessor::new);
}

The createProcessor method is passed a function that can create a processor from a PooledProcessorConfig. For the PooledTemplateProcessor, its constructor is passed using PooledTemplateProcessor::new.

<T> T createProcessor(Function<PooledProcessorConfig, T> factory) {
  PooledProcessorConfig config = new PooledProcessorConfig(
    context, rootFragment, localContext, contextStackRequired,
    userDirectives, featureSet, directiveLookup, resourceBundleName,
    templateObjectMapper, templateObjectProviderMap
  );
  return factory.apply(config);
}

The generic type is not currently restricted in any further way. It is not yet clear whether this method will be used only for PooledTemplateProcessor and HookedTemplateProcessor, or for other mechanisms. It is also not entirely clear whether the template’s private attributes are returned directly or whether they are copied. Since this method can be called by multiple processors, perhaps the entire template generation should take place within the processor call. However, there is still some time before FreshMarker 3.0.0 is released.

The heart of the new template processing is hidden in the process method of the PooledTemplateProcessor. It differs slightly from the implementation in Template because here the Environment and the Writer are reused.

public String process(Map<String, Object> dataModel) {
  pooledEnvironment.reset(dataModel);
  pooledWriter.reset();
  ProcessContext processContext = new ProcessContext(
    staticContext, pooledEnvironment, userDirectives,
    pooledWriter, featureSet, templateObjectMapper,
    localContext, directiveLookup, contextStackRequired
  );
  processContext.setResourceBundle(resourceBundleName);
  try {
    rootFragment.process(processContext);
  } catch (TemplateReturnException e) {
    // normal flow control
  }
  return pooledWriter.toString();
}

Resetting the environment—and the writer in particular—has a significant impact on FreshMarker‘s performance. This is evident in the results of the Template-Engine Benchmarks.

The PooledTemplateProcessor improves performance by 10% compared to the template engine’s normal processing speed. However, such a benchmark is not a single metric that can fully describe a template engine’s performance. Performance depends heavily on how frequently templates are called, how homogeneous the output is, how complex the templates are structured, etc. In this benchmark, FreshMarker performs well because the templates are processed serially and always produce the same output. After the first pass, the pooled writer has already allocated sufficient memory for the output, and each subsequent call does not require new memory for the output.

If your application fits this scenario, you can look forward to a performance boost in the template engine.

Leave a Comment