Skip to content

2.19 variable performance regression #86881

@Vladimir-csp

Description

@Vladimir-csp

Summary

I'm seeing a significant performance regression for templated variables after upgrade from 2.18 to 2.19. An example I have degraded from 0.28s to 10.98s

Issue Type

Bug Report

Component Name

core

Ansible Version

ansible [core 2.19.8]
  python version = 3.14.4 (main, Apr 16 2026, 22:56:09) [Clang 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd7080 (/usr/local/bin/python3.14)
  jinja version = 3.1.6
  pyyaml version = 6.0.3 (with libyaml v0.2.5)

Configuration

# if using a version older than ansible-core 2.12 you should omit the '-t all'
$ ansible-config dump --only-changed -t all

ANSIBLE_PIPELINING(/usr/local/etc/ansible/ansible.cfg) = True
CALLBACKS_ENABLED(/usr/local/etc/ansible/ansible.cfg) = ['profile_tasks']
DEFAULT_BECOME_FLAGS(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E
DEFAULT_FORKS(/usr/local/etc/ansible/ansible.cfg) = 15
DEFAULT_HASH_BEHAVIOUR(/usr/local/etc/ansible/ansible.cfg) = replace
DEFAULT_LOAD_CALLBACK_PLUGINS(/usr/local/etc/ansible/ansible.cfg) = False
DEFAULT_LOG_PATH(/usr/local/etc/ansible/ansible.cfg) = /data/log/ansible/ansible.log
DEFAULT_POLL_INTERVAL(/usr/local/etc/ansible/ansible.cfg) = 15
DEFAULT_REMOTE_USER(/usr/local/etc/ansible/ansible.cfg) = ansible
DEFAULT_TIMEOUT(/usr/local/etc/ansible/ansible.cfg) = 30
EDITOR(env: EDITOR) = mcedit
HOST_KEY_CHECKING(/usr/local/etc/ansible/ansible.cfg) = True
INJECT_FACTS_AS_VARS(/usr/local/etc/ansible/ansible.cfg) = False
INTERPRETER_PYTHON(/usr/local/etc/ansible/ansible.cfg) = auto_silent
INVENTORY_ENABLED(/usr/local/etc/ansible/ansible.cfg) = ['ini', 'script']
PAGER(env: PAGER) = less
PERSISTENT_CONNECT_TIMEOUT(/usr/local/etc/ansible/ansible.cfg) = 30
TRANSFORM_INVALID_GROUP_CHARS(/usr/local/etc/ansible/ansible.cfg) = ignore
USE_PERSISTENT_CONNECTIONS(/usr/local/etc/ansible/ansible.cfg) = True

GALAXY_SERVERS:


BECOME:
======

runas:
_____
become_flags(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E

su:
__
become_flags(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E

sudo:
____
become_flags(/usr/local/etc/ansible/ansible.cfg) = -H -S -n -E

CALLBACK:
========

default:
_______
result_format(/usr/local/etc/ansible/ansible.cfg) = yaml

minimal:
_______
result_format(/usr/local/etc/ansible/ansible.cfg) = yaml

OS / Environment

FreeBSD 14.3 control host, Ubuntu 24.04 client

Steps to Reproduce

An example of a variable that combines data from a dynamic inventory (each host receives a dict with inventory data), any playbook-level host_vars/group_vars modifiers/options/amendments/corrections/overrides, actual addresses from gathered facts into a finalized source-of-truth interfaces data structure accessed from a ton of roles/playbooks.

---
heavy50__interfaces_combined: |-
  {% set ns = namespace(out={}, interfaces_precombined={}, fact_v4=[], fact_v6=[]) %}
  {# assemble interface defaults #}
  {% set interface_defaults_combined = interface_defaults | combine(*(query('vars', *(query('varnames', '^interface_defaults_amend__.+') | sort))), recursive=True) %}
  {# preassemble interfaces with tweaks #}
  {% set ns.interfaces_precombined = interfaces|default({}) | combine(*(query('vars', *(query('varnames', '^interfaces_amend__.+') | sort))), recursive=True) %}
  {# if dynamic_inventory is enabled in defaults, also add other ifaces from dynamic_inventory as empty shells for iteration #}
  {% if interface_defaults_combined.dynamic_inventory|default(False)|bool %}
  {% for item in dynamic_inventory.interfaces.keys()|default([]) if item not in ns.interfaces_precombined %}
  {% set _ = ns.interfaces_precombined.update({item: {}}) %}
  {% endfor %}
  {% endif %}
  {# iterate over precombined interfaces #}
  {% for item, values in ns.interfaces_precombined|dictsort %}
  {# decide if to load info from dynamic_inventory #}
  {% set from_dynamic_inventory = values.dynamic_inventory|default(interface_defaults_combined.dynamic_inventory)|default(False)|bool and dynamic_inventory.interfaces[item] is defined %}
  {% if from_dynamic_inventory %}
  {# determine if to include ips of child ifaces #}
  {% set from_dynamic_inventory_child_ips = values.dynamic_inventory_child_ips|default(interface_defaults_combined.dynamic_inventory_child_ips)|default(True)|bool %}
  {# load info from dynamic_inventory, first pass: ips and ifnames #}
  {% set values_from_dynamic_inventory =
    {} | combine(
      {'ip4': [], 'ip6': []},
      dynamic_inventory.interfaces[item]
        | dict2items
        | selectattr('key', 'in', ['ifname', 'ip4', 'ip6'])
        | selectattr('value')
        | items2dict,
      dynamic_inventory.children_inv
        | map('extract', hostvars, ['dynamic_inventory', 'interfaces'])
        | map('dict2items')
        | flatten
        | map(attribute='value')
        | selectattr('parent', 'equalto', item)
        | map('dict2items')
        | map('selectattr', 'key', 'in', ['ip4', 'ip6'] )
        | map('items2dict')
        | list
        if from_dynamic_inventory_child_ips else {},
      list_merge='append_rp'
    )
  %}
  {# second pass: additional and derivative values #}
  {% set _ = values_from_dynamic_inventory.update(
    {
      "dns4":
        dynamic_inventory_global.networks[
          values_from_dynamic_inventory.ip4[0]|default('noip', True)
          | ansible.utils.ipv4('network/prefix')
        ].dns|default(['noip'], True) | ansible.utils.ipv4('address'),
      "dns6":
        dynamic_inventory_global.networks[
          values_from_dynamic_inventory.ip6[0]|default('noip', True)
          | ansible.utils.ipv6('network/prefix')
        ].dns|default(['noip'], True) | ansible.utils.ipv6('address'),
      "ip4_gateway":
        dynamic_inventory_global.networks[
          values_from_dynamic_inventory.ip4[0]|default('noip', True)
          | ansible.utils.ipv4('network/prefix')
        ].gw|default('noip', True) | ansible.utils.ipv4('address'),
      "ip6_gateway":
        dynamic_inventory_global.networks[
          values_from_dynamic_inventory.ip6[0]|default('noip', True)
          | ansible.utils.ipv6('network/prefix')
        ].gw|default('noip', True) | ansible.utils.ipv6('address'),
      "ip4_method":
        ('sync-auto' if ansible_facts.system == 'FreeBSD' else 'auto')
        if dynamic_inventory.interfaces[item].dhcp|default(False)
        else 'manual'
        if values_from_dynamic_inventory.ip4|default([]) is truthy
        else '',
      "ip6_method":
        ('sync-auto' if ansible_facts.system == 'FreeBSD' else 'auto')
        if dynamic_inventory.interfaces[item].dhcp6|default(False)
        else 'manual'
        if values_from_dynamic_inventory.ip6|default([]) is truthy
        else '',
    }
      | dict2items
      | selectattr('value')
      | items2dict,
  ) %}
  {% else %}
  {# empty data if not loading info from dynamic_inventory #}
  {% set values_from_dynamic_inventory = {} %}
  {% endif %}
  {# special treatment for ifname #}
  {% set ifname = values.ifname|default(values_from_dynamic_inventory.ifname, True)|default(item, True) %}
  {# insert dhcp addresses from ansible_facts #}
  {% set ns.fact_v4 = [] %}{% set ns.fact_v6 = [] %}
  {% if ifname is truthy %}
  {% for addr_dict in [ansible_facts[ifname].ipv4|default({})]|flatten + ansible_facts[ifname].ipv4_secondaries|default([]) %}
  {% set addr = '{}/{}'.format(addr_dict.address|default('noip'), addr_dict.netmask|default('noip')) | ansible.utils.ipv4('cidr') %}
  {% set _ = ns.fact_v4.append(addr) if addr is truthy else False %}
  {% endfor %}
  {% endif %}
  {% if ifname is truthy %}
  {% for addr_dict in ansible_facts[ifname].ipv6|default([])|flatten %}
  {% set addr = '{}/{}'.format(addr_dict.address|default('noip'), addr_dict.prefix|default('noip')) | ansible.utils.ipv6('cidr') %}
  {% set _ = ns.fact_v6.append(addr) if addr is truthy else False %}
  {% endfor %}
  {% endif %}
  {# finalize iface and prepend nameservers if any #}
  {% set _ = ns.out.update(
    {
      item:
        interface_defaults_combined
        | combine(
          values_from_dynamic_inventory,
          values|default({}, True),
          {"ifname": ifname, "ip4_fact": ns.fact_v4, "ip6_fact": ns.fact_v6},
          recursive=True
        )
        | combine(
          {
            "dns4": nameservers_prepend|default(['noip']) | ansible.utils.ipv4('address'),
            "dns6": nameservers_prepend|default(['noip']) | ansible.utils.ipv6('address'),
          },
          list_merge='prepend_rp'
        )
    }
  ) %}
  {# end of iface iteration #}
  {% endfor %}
  {{ ns.out }}

Expected Results

Renders in a fraction of a second
works on:

ansible [core 2.18.7]
  python version = 3.14.3 (main, Apr  2 2026, 22:54:57) [Clang 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd7080 (/usr/local/bin/python3.14)
  jinja version = 3.1.6
  libyaml = True

Actual Results

Renders in 10 seconds

Code of Conduct

  • I agree to follow the Ansible Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    affects_2.18affects_2.19bugThis issue/PR relates to a bug.needs_triageNeeds a first human triage before being processed.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions