﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
33098	Micro-optimisation for functional.keep_lazy for single argument uses.	Keryn Knight	Keryn Knight	"`keep_lazy` has an internal decorator function, `wrapper` which may get called a lot during template rendering, because it's ultimately used by `conditional_escape` which is in turn used by `render_value_into_context`.

Rendering the standard admin change form for a user via `client.get(f""/auth/user/1/change/"")` 100 times gives me the following cprofile output:
{{{
   11694527 function calls (11041473 primitive calls) in 8.609 seconds
   Ordered by: internal time
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
30200/300    0.307    0.000    6.703    0.022 defaulttags.py:160(render)
     6434    0.242    0.000    0.250    0.000 {built-in method io.open}
   174600    0.218    0.000    0.682    0.000 base.py:849(_resolve_lookup)
   194000    0.204    0.000    1.120    0.000 base.py:698(resolve)
29200/500    0.175    0.000    6.720    0.013 loader_tags.py:168(render)
    30302    0.162    0.000    0.735    0.000 base.py:654(__init__)
    34240    0.161    0.000    0.355    0.000 base.py:779(__init__)
15137/5509    0.157    0.000    1.555    0.000 base.py:455(parse)
    91639    0.144    0.000    0.542    0.000 functional.py:226(wrapper).  # 1.6%?
...
}}}
with `wrapper` being the important line, and then:
{{{
In [1]: from django.utils.html import escape
In [2]: %timeit escape('<abc>d&g</abc>')
2.2 µs ± 51.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
}}}

This is because the current implementation is:
{{{
if any(isinstance(arg, Promise) for arg in itertools.chain(args, kwargs.values())):
    return lazy_func(*args, **kwargs)
return func(*args, **kwargs)
}}}
that is, it's optimised for the general case of N arguments. But Django's internal usage of `keep_lazy` is nearly unanimously on functions with a single argument.

It is possible to derive (at decorator time rather than call time) whether or not the function being wrapped needs to support multiple arguments, via `inspect.signature` and dispatch to a different wrapper, which offers somewhat better performance.

A super naive implementation looks like:
{{{
@wraps(func)
def keep_lazy_single_argument_wrapper(arg):
    if isinstance(arg, Promise):
        return lazy_func(arg)
    return func(arg)

@wraps(func)
def keep_lazy_multiple_argument_wrapper(*args, **kwargs):
    if any(isinstance(arg, Promise) for arg in itertools.chain(args, kwargs.values())):
        return lazy_func(*args, **kwargs)
    return func(*args, **kwargs)

if len(inspect.signature(func).parameters) == 1:
    return keep_lazy_single_argument_wrapper
else:
    return keep_lazy_multiple_argument_wrapper
}}}
which provides the best difference:
{{{
11327832 function calls (10674778 primitive calls) in 9.062 seconds
   Ordered by: internal time
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
...
    91639    0.059    0.000    0.339    0.000 functional.py:227(keep_lazy_single_argument_wrapper). # 0.6% of time
}}}
and the actual usage time:
{{{
In [2]: %timeit escape('<abc>d&g</abc>')
1.5 µs ± 16.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
}}}
Though it comes at the cost of no longer being able to use keyword arguments:
{{{
In [3]: escape(text=1)
TypeError: keep_lazy_single_argument_wrapper() got an unexpected keyword argument 'text'
}}}
which can be worked around by doing something (back of the napkin) like:
{{{
func_params = inspect.signature(func).parameters
func_first_param_name = tuple(func_params.keys())[0]

@wraps(func)
def keep_lazy_single_argument_wrapper(*args, **kwargs):
    if (args and isinstance(args[0], Promise)) or ('func_first_param_name' in kwargs and isinstance(kwargs.get(func_first_param_name), Promise)):
        return lazy_func(*args, **kwargs)
    return func(*args, **kwargs)
...
}}}
which still seems to be better:
{{{
   11327896 function calls (10674842 primitive calls) in 9.411 seconds
   Ordered by: internal time
   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
...
    91639    0.088    0.000    0.382    0.000 functional.py:230(keep_lazy_single_argument_wrapper). # 0.9%
}}}
and:
{{{
In [2]: %timeit escape('<abc>d&g</abc>')
1.64 µs ± 32.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [3]: escape(text=1)
'1'
}}}
**and** correctly works with invalid kwargs (this is as much a note to myself as anything, my previous attempts did not :)):
{{{
In [4]: escape(test=1)
TypeError: escape() got an unexpected keyword argument 'test'
}}}"	Cleanup/optimization	assigned	Template system	dev	Normal				Accepted	1	0	0	1	0	0
