Using date template filter for thousands of instances - Solved

Howdy fellow Djangonauts,

I recently ran into a performance problem that was new to me. It stemmed from using the date template filter thousands of time in a for loop within a template. On our production machine, it took 8s to render the template via render_to_string.

My question is, has anyone else ever ran into this and/or is this a known issue? It appears that the culprit is django.utils.dateformat.format.

The effective code is:

{% for value in values %}
<tr>
  <td>
    {{ value|date:"DATE_FORMAT" }}
    <br />{{ value|time:"TIME_FORMAT" }}
  </td>
</tr>
{% endfor %}

I just benchmarked those filters and they are very fast - about 10-20 microseconds per iteration, with the default format, Python 3.9, and Django 3.1:

$ python manage.py shell
Python 3.9.0 (default, Oct 10 2020, 10:28:14)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.18.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from django.template.defaultfilters import date

In [2]: import datetime as dt

In [3]: now = dt.datetime.utcnow()

In [4]: date(now)
Out[4]: 'Oct. 19, 2020'

In [5]: %timeit date(now)
20.3 µs ± 252 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [6]: from django.template.defaultfilters import time

In [7]: time(now)
Out[7]: '4:49 p.m.'

In [8]: %timeit time(now)
13.6 µs ± 318 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

I get the same results if I provide "DATE_FORMAT" / "TIME_FORMAT" as arguments, although my settings don’t contain them.

With this speed, even for thousands of objects you shouldn’t be reaching 8 seconds - 1000 iterations at 20 microseconds totals just 0.02 seconds.

Can you try using %timeit on a Template.render() with that chunk to show the slowness? And then run a profiler like py-spy or austin to spot the slow parts? Perhaps there’s something about t

1 Like

Good call. It doesn’t appear to be related to the template filter. Seems like a context issue.

Using the following template:

<div>
  {% for value, others in values %}
    <p>{{ value }}</p>
    <ol>
    {% for other in others %}
      <li>{{ other|date:"Y/n/j g:i A" }}</li>
    {% endfor %}
    </ol>
  {% endfor %}
</div>

And the following context:

context = {"values": [(i, [now - timedelta(minutes=j) for j in range(4)]) for i in range(10000)]}

Produces

In [49]: %timeit x = template.render(context, None)
3.64 s ± 40.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

If I remove the date template filter for {{ other }} and use a more basic context:

context = {"values": [(i, list(range(4))) for i in range(10000)]}

Produces

In [43]: %timeit x = template.render(context, None)
957 ms ± 6.23 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

The py-spy’s top sorted by Own %.

Total Samples 383
GIL: 0.00%, Active: 100.00%, Threads: 1

  %Own   %Total  OwnTime  TotalTime  Function (filename:line)                                                                                                                                               
 11.00%  17.00%   0.250s    0.310s   format (django/utils/dateformat.py:43)
  6.00%   6.00%   0.140s    0.140s   __getattr__ (asgiref/local.py:107)
  5.00%   6.00%   0.120s    0.130s   format (django/utils/dateformat.py:34)
  4.00%   4.00%   0.040s    0.040s   wrapper (django/utils/functional.py:220)
  4.00%   4.00%   0.050s    0.050s   _get_context_id (asgiref/local.py:72)
  4.00%   5.00%   0.130s    0.180s   _get_context_id (asgiref/local.py:51)
  3.00%   3.00%   0.030s    0.030s   _subx (re.py:327)
  3.00%   5.00%   0.040s    0.070s   get_current_task (asgiref/sync.py:351)
  2.00%  18.00%   0.020s    0.470s   __text_cast (django/utils/functional.py:140)
  2.00%  20.00%   0.020s    0.350s   get_language (django/utils/translation/trans_real.py:303)
  2.00%   2.00%   0.020s    0.020s   render (django/template/defaulttags.py:204)
  2.00%   2.00%   0.020s    0.020s   inner (django/utils/functional.py:241)
  2.00%  72.00%   0.020s     1.46s   date_format (django/utils/formats.py:152)
  2.00%   2.00%   0.020s    0.020s   gettext (django/utils/translation/trans_real.py:352)
  2.00%  22.00%   0.030s    0.380s   get_language (django/utils/translation/__init__.py:254)
  2.00%   2.00%   0.030s    0.030s   current_task (asyncio/tasks.py:37)
  2.00%   2.00%   0.020s    0.020s   render (django/template/defaulttags.py:210)
  2.00%   2.00%   0.020s    0.020s   __getitem__ (django/template/context.py:80)

I’ve also included a screenshot of the speedscope results. I don’t feel comfortable uploading the raw file given I’m unsure of what’s all included in it. Generally, I don’t see any quick wins. It looks like I need to rework the application to render this page piecemeal.

There’s a lot of repetitive calculations in that nested comprehension.
[now - timedelta(minutes=j) for j in range(4)]
You’re repeating this 10,000 times to recalculate the same set of values

Even your simplified form is creating a different list for each iteration - is that really necessary? (If not, I’m sure there’s some benefit to be gained by reusing the same list)

What if you were to try:

delta_list =  [now - timedelta(minutes=j) for j in range(4)]
context = {"values": [(i, delta_list) for i in range(10000)]}

On my laptop:

In [66]: %timeit context = {"values": [(i, [now - timedelta(minutes=j) for j in range(4)]) for i in range(10000)]}
45.5 ms ± 131 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

vs

In [67]: delta_list = [now-timedelta(minutes=j) for j in range(4)]
...
In [69]: %timeit context = {"values": [(i, delta_list) for i in range(10000)]}
740 µs ± 2.45 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

about 1/50th of the time.

Ken

Ah, I wrote the sample data code wrong. I meant to have different datetime values for each instance in the list.

Edit, no I think what I have works well enough for the example. It’s obviously not great, but it works for its purpose.

We’re reworking the page. Finally took a look at the size of the response and it’s at 15MB. Yikes.

Yea… that’s never good. You’re going to encounter all other sorts of delays as well regarding rendering speeds in the browser.
(I’ve got a page I’m working on now that is rendering about 8 MB - time to paginate…)

Yup! I started in on this from, “why’s this page timing out?”. Finally asked, what does it look like with just little enough data to not time out?