Cache Handlebars runtime and compiled templates per generation (~4.5x speed-up)#12746
Open
human-unidentified wants to merge 1 commit into
Open
Cache Handlebars runtime and compiled templates per generation (~4.5x speed-up)#12746human-unidentified wants to merge 1 commit into
human-unidentified wants to merge 1 commit into
Conversation
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12745
Summary
HandlebarTemplateEngine.getRendered(...)previously rebuilt theHandlebarsinstance 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)Handlebarsinstance lazily on first use and reuse it for the lifetime of one generation. Helpers are registered once.TemplatepertemplateFile, so entry templates (controller, model, …) are parsed once even though they are rendered many times.IndentAwareTemplateCacheso partials referenced via{{> ... }}are parsed once per unique(filename, applied-indent)pair.IndentAwareTemplateCache.java(new, 96 lines)ConcurrentMapTemplateCache.Partial.mergewraps the partial'sTemplateSourcevia an anonymous class whoseequals/hashCodedelegate to the underlying source (filename + lastModified), while itscontent()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.(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-generatorschange.Performance
Measured on a realistic OpenAPI spec (~75 controllers, 178 output files), warm runs, 3 iterations each:
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 stockConcurrentMapTemplateCacheproduced silently shifted whitespace in scenarios where the same partial was included at multiple indent levels.Out of scope (intentionally)
Partial.mergeindent-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.