Skip to content

Cache Handlebars runtime and compiled templates per generation (~4.5x speed-up)#12746

Open
human-unidentified wants to merge 1 commit into
swagger-api:3.0.0from
human-unidentified:perf/cache-handlebars
Open

Cache Handlebars runtime and compiled templates per generation (~4.5x speed-up)#12746
human-unidentified wants to merge 1 commit into
swagger-api:3.0.0from
human-unidentified:perf/cache-handlebars

Conversation

@human-unidentified
Copy link
Copy Markdown

Fixes #12745

Summary

HandlebarTemplateEngine.getRendered(...) previously rebuilt the Handlebars instance and reparsed the template (and every partial transitively referenced) on each invocation. JFR profiles of a realistic OpenAPI spec show the ANTLR-based jknack lexer accounting for ~65% of CPU — the dominant cost was repeated parsing, not rendering.

This PR caches both the Handlebars runtime and compiled templates for the lifetime of one generation.

Changes

  • HandlebarTemplateEngine.java (+49 / −12)

    • Build the Handlebars instance lazily on first use and reuse it for the lifetime of one generation. Helpers are registered once.
    • Cache the compiled top-level Template per templateFile, so entry templates (controller, model, …) are parsed once even though they are rendered many times.
    • Install IndentAwareTemplateCache so partials referenced via {{> ... }} are parsed once per unique (filename, applied-indent) pair.
  • IndentAwareTemplateCache.java (new, 96 lines)

    • Replacement for jknack's ConcurrentMapTemplateCache. Partial.merge wraps the partial's TemplateSource via an anonymous class whose equals/hashCode delegate to the underlying source (filename + lastModified), while its content() is re-indented by the include site's leading whitespace. The same partial included at different indents therefore collides on lookup in the stock cache and the first-compiled indent wins for every subsequent include — observable as silently shifted whitespace in generated output. See related jknack issues #401 and #708.
    • The replacement keys on (filename, content) so two wrappers with the same filename but different applied indent get separate entries. Compilation still happens at most once per unique (template, indent) pair, which preserves the bulk of the speed-up.

No public API changes. No template changes. No swagger-codegen-generators change.

Performance

Measured on a realistic OpenAPI spec (~75 controllers, 178 output files), warm runs, 3 iterations each:

baseline with this change speed-up
average ~55 s ~12 s ~4.5× (−78%)

The same cache also avoids re-registering helpers and re-instantiating the ANTLR runtime per render, which compounds the savings on smaller specs.

Tests

  • mvn ... -Prun-dotnet-unit-tests → 6/6 pass.
  • mvn ... -Prun-dotnet-integration-tests → 27/27 generator scenarios match expected output byte-for-byte, no fixture changes required. The indent-aware key for the partial cache is essential here: an earlier prototype that used the stock ConcurrentMapTemplateCache produced silently shifted whitespace in scenarios where the same partial was included at multiple indent levels.

Out of scope (intentionally)

  • No jknack handlebars upgrade. The underlying Partial.merge indent-collision behavior is present on the latest 4.5.1 release as well; a workaround at the cache layer keeps this PR small and reviewable. Upgrading the dependency is a separate piece of work.

Reference

#12313 / its accompanying fix established the same precedent in another hotspot (AbstractJavaCodegen.toModelName() was called millions of times and was cached for a 3× improvement). This PR applies the same idea to template parsing.

… speed-up)

HandlebarTemplateEngine.getRendered() previously rebuilt the Handlebars
instance and re-parsed the template (and every partial transitively
referenced) on each invocation. JFR profiles of a realistic OpenAPI
spec show the ANTLR-based jknack lexer accounting for ~65% of CPU -
the dominant cost was repeated parsing, not rendering.

Changes:
  * Build the Handlebars instance lazily on first use and reuse it
    for the lifetime of one generation. Helpers are registered once.
  * Cache the compiled top-level Template per templateFile so entry
    templates (controller, model, ...) are parsed once even though
    they are rendered many times.
  * Install IndentAwareTemplateCache so partials referenced via
    {{> ... }} are parsed once per unique (filename, applied-indent)
    pair. The stock ConcurrentMapTemplateCache from jknack cannot
    be used here: Partial.merge wraps the partial's TemplateSource
    via an anonymous class whose equals/hashCode delegate only to
    the underlying source, while its content() is re-indented by
    the include site. The same partial included at different indents
    therefore collides on lookup and the first-compiled indent wins
    for every subsequent include - observable as silently shifted
    whitespace in generated output. See related jknack issues swagger-api#401
    and swagger-api#708. The replacement cache keys on (filename, content),
    which keeps the speed-up while preserving correct indentation.

Measured on a realistic OpenAPI spec (~75 controllers, 178 output
files), warm runs:

  baseline: ~55 s average (3 warm runs)
  patched : ~12 s average (3 warm runs)
  speedup : ~4.5x (-78%)

Tests:
  * run-dotnet-unit-tests        -> 6/6 pass
  * run-dotnet-integration-tests -> 27/27 scenarios pass byte-for-byte,
    no fixture changes required.

No public API changes. No template changes. No changes to
swagger-codegen-generators are required.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant