perf(template): cache the variables of a string template per template text

AJ
6d ago
+124-1

Cache the variables of a string Jinja2 template per template text

Infrahub reads the variables of a display-label template each time it loads a node whose schema declares one. Performance testing of GraphQL queries found that this parsing took a large share of the response time (comment on opsmill/infrahub#11252). In review, the cache was moved from Infrahub into the SDK (review comment).

Before this change, Jinja2Template.get_variables() built a SandboxedEnvironment with all filters and parsed the template on every call. Jinja2's own cache (Environment.cache) applies only to templates loaded by name through a loader, not to parse() or from_string().

Changes

  • New public function get_template_variables(template: str) -> list[str] in infrahub_sdk.template. It returns the sorted variable names of a string template and caches them per template text (lru_cache, 1024 entries). It returns a new list on each call.
  • Jinja2Template.get_variables() uses this function for string templates. File-based templates are still parsed on every call, because a file's contents can change.
  • The cache holds only the tuple of variable names. Rendering, get_template() and validate() are unchanged and use the environment of each Jinja2Template object.

Behavior change: unknown filters

To collect variables, Jinja2 compiles the template, and compiling fails with No filter named '<name>' for any filter the environment lacks. The variables do not depend on which filters exist, so on a cache miss the function parses with the same environment type as a string template object and registers a placeholder only for filter names that the environment lacks. The placeholder is never used for rendering. If Jinja2 calls it while folding constants, it raises, and Jinja2 then leaves that expression unfolded.

As a result, for a string template, get_variables() no longer raises JinjaTemplateSyntaxError when the template uses a filter that the template object does not provide. validate() still rejects that filter: every filter it allows is present in the object's environment. render() also still fails on it. An unknown test (is <name>) still raises.

Measurements (local, two-variable template)

CallBeforeAfter, repeated template text
Jinja2Template(template=t).get_variables()about 139 µsabout 10 µs
get_template_variables(t)not availableabout 0.07 µs

Tests

tests/unit/sdk/test_template.py (40 passed) covers:

  • The existing string cases, through the new function.
  • Unknown filters, a filter block, and an unknown filter on a constant.
  • Syntax errors, after another template has been cached.
  • An unknown test name still raises.
  • Changing a returned list does not change the cache.
  • Two template objects with different filters render the same template text with their own filters.

ruff, ty and mypy pass.

Follow-up

After this PR merges, a separate Infrahub PR against release-1.12 will update the python_sdk submodule and call get_template_variables() from DisplayLabel, BaseNodeSchema.get_derived_field_paths() and the uniqueness violation message.

🤖 Generated with Claude Code