#33098: Micro-optimisation for functional.keep_lazy for single argument uses.
-------------------------------------+-------------------------------------
               Reporter:  Keryn      |          Owner:  Keryn Knight
  Knight                             |
                   Type:             |         Status:  assigned
  Cleanup/optimization               |
              Component:  Template   |        Version:  dev
  system                             |
               Severity:  Normal     |       Keywords:
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 `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'
 }}}

-- 
Ticket URL: <https://code.djangoproject.com/ticket/33098>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/052.8a1ec986f7a635ac3e352c76497c433c%40djangoproject.com.

Reply via email to