Edgewall Software

Version 26 (modified by Christian Boos, 8 years ago) ( diff )

updated status (all tests pass)

Switch to the Jinja2 Template Engine

We've decided some time ago to remove the legacy support for the ClearSilver template engine for Trac 1.0 (r10570). Clearsilver had its share of inconveniences, enough that we decided to switch to the nicer Genshi template engine in 0.11, but ClearSilver was very fast and memory lenient. While we managed to keep Genshi memory usage somewhat in control (#6614), the speed was never really adequate, especially for big changesets and for displaying source files over a few thousand lines of code (see TracDev/Performance#Genshi for details).

So one solution would be to switch template engine once again, to one that would combine the advantages of Genshi (pure Python, nice templates, flexible) and ClearSilver (speed!). Such a beast seems to exist now: Jinja2.

Overview of activities and progress

There's an experimental branch which supports this proposal: cboos.git@jinja2
(mirror available in github - https://travis-ci.org/cboos/trac.svg on Travis, https://ci.appveyor.com/api/projects/status/kqgv4awct01hsl7t/branch/jinja2 on AppVeyor)

Status of the branch (2016-03-20):

  • ported 66% of the Genshi templates, the automated tests should stays green
  • DONE clarify upgrade path for plugins that came to rely on ITemplateStreamFilters? 127/898 plugins (14.1%) on trac-hacks.org use filter_stream() → see replacing ITemplateStreamFilter
  • DONE clarify how to handle themeing? → see HtmlTemplates#Jinjaarchitecture
  • DONE rewrite tag builders or use lightweight string templates? → tag Fragment/Element builder API has been reimplemented
  • TODO accesskey support
  • TODO hack ITemplateStreamFilter support for Jinja2 templates
  • TODO site.html replacement, for example try to reproduce t.e.o customizations
  • others?

See also this Trac-Dev discussion from 2010, which is still pertinent. While we managed to release Genshi 0.6 since then, the issue is a recurring one, see this recent (2016-01) Genshi question on Trac-Users.

The topic is now (Feb / March 2016) again discussed on trac-dev.

The Jinja2 template engine has the peripheral benefit of being used by the Django community: trac-dev/KqWPQWuZ63k/GPfda0_PDgAJ.

Experimenting with Jinja2 (2.8)

Nothing like a few numbers to make a point.

These are the timings for rendering r3871 (don't try this one here, please), with the diff options set to side-by-side, in place modifications, served by tracd on my development laptop. This generates a page weighing from 11.5MB (Genshi) to 10.3MB (Jinja2) in size.

Genshi Jinja2
stream blob generate stream (5) stream (10) stream (100) stream (1000) blob
1st 2nd 1st 2nd 1st 2nd 1st 2nd 1st 2nd 1st 2nd 1st 2nd 1st 2nd
TTFB 1660015670 25530 244602020 11602030 11602070 11702150 12302280 12303370 2450
CD 1609016050 387 12402820 27202730 26402730 26802470 23902350 2250 488 1060
Total3269031720 25917 257004840 38804760 38004800 38504620 36204630 34803850 3510
Rdr 235332327314771263

Some explanations:

  • Genshi (0.7 with speedups)
    • stream means we return content via Stream.serialize and send chunks as we have them
    • blob means we first generate all the content in memory with Stream.render, then send it at once
  • Jinja2 (2.8 with speedups)
    • generate means we use Template.generate and send chunks as we have them
    • stream means we use the TemplateBuffer wrapper on the above, which groups a few chunks (given by the number in parentheses) together before we send them; for a chunk size of 100, we get the best compromise: still a very low TTFB and a reduced Content download time; actually the sweet spot is probably between 10 and 100, and will most certainly depend on the actual content (I just tested 75 which gives 1160/2430 for example)
    • blob means we first generate all the content in memory with Template.render
  • both:
    • 1st, is the time in ms for the first request, sent right after a server restart
    • 2nd, is the time in ms for the second request, sent just after the first (usually the 3rd and subsequent requests would show the same results as this 2nd request)

We measure:

  • TTFB (Time to first byte), as given by Chrome network panel in the developer window
  • CD (Content download), idem
  • Rdr (template rendering time), mostly significant for the "blob" method otherwise it also takes the network latency into account

All values are given in milliseconds.

Note that even if the total "blob" time seems better than the total "stream" one, the lower TTFB is nevertheless a major benefit for the streaming variant, as this means the secondary requests can start earlier. In this case, it can finish before the main request.

In addition, while I didn't measure precisely the memory usage, Genshi made the python.exe process jump from 109MB to 239MB while rendering the request (blob). The memory seems to be freed afterwards (there were no concurrent requests). By contrast, with Jinja2 the memory spike was 106MB to 126MB.

In another experiment, I used the memory_profiler on Windows, which provided the following results when rendering a big changeset, for a rendered page weighing 4.48MB (it was a side-by-side diff):

Genshi xhtml: 293.612 total (load=0.017, generate=0.017, filter=0.000, render=293.578)
Filename: d:\Trac\repos\trunk\trac\web\chrome.py

Line #    Mem usage    Increment   Line Contents
================================================
  1283    101.6 MiB      0.0 MiB               @profile
  1284                                         def genshi():
  1285    101.6 MiB      0.0 MiB                   buffer = StringIO()
  1286    101.6 MiB      0.0 MiB                   t5 = time.time()
  1287    101.6 MiB      0.0 MiB                   stream.render(method, doctype=doctype, out=buffer,
  1288    153.2 MiB     51.6 MiB                                 encoding='utf-8')
  1289    158.0 MiB      4.9 MiB                   gs = buffer.getvalue().translate(_translate_nop,
  1290    158.0 MiB      0.0 MiB                                                    _invalid_control_chars)
  1291    158.0 MiB      0.0 MiB                   t6 = time.time()
  1292    158.0 MiB      0.0 MiB                   show_times('Genshi', t2 - t1, t4 - t3, t5a - t4a, t6 - t5,
  1293    158.1 MiB      0.0 MiB                              method)
  1294    158.1 MiB      0.0 MiB                   return gs

vs.

Line #    Mem usage    Increment   Line Contents
================================================
  1255    101.6 MiB      0.0 MiB               @profile
  1256                                         def jinja(mode='render'):
  1257    101.6 MiB      0.0 MiB                   if jtemplate:
  1258    101.6 MiB      0.0 MiB                       j5 = time.time()
  1259    101.6 MiB      0.0 MiB                       if mode == 'render':
  1260    111.1 MiB      9.4 MiB                           js = jtemplate.render(jdata)
  1261    111.1 MiB      0.0 MiB                           j5a = time.time()
  1262    115.4 MiB      4.3 MiB                           js = js.encode('utf-8') \
  1263    115.4 MiB      0.0 MiB                                  .translate(_translate_nop,
  1264    106.7 MiB     -8.7 MiB                                             _invalid_control_chars)
  1265    106.7 MiB      0.0 MiB                           j6 = time.time()
  1266    106.7 MiB      0.0 MiB                           show_times('Jinja2', j2 - j1, 0, j5a - j5, j6 - j5a,
  1267    106.7 MiB      0.0 MiB                                      'html')
  1268    106.7 MiB      0.0 MiB                           return js

In summary, this means that for the big problematic pages, we can easily have a 10x speedup and more, by migrating to Jinja2, and this with a much lighter memory footprint. For smaller pages, the speed-up is between 5x to 10x as well.

Genshi to Jinja2 Migration

Some systematic comparison of the Genshi and Jinja2 template syntax can be seen in PortingFromGenshiToJinja#Changesinthetemplatesyntax (that page was modeled after the old PortingFromClearSilverToGenshi page).

See also PortingFromGenshiToJinja/Example for a full example presented side-by-side.

To facilitate the creation of error-free Jinja2 templates for HTML (or XML), we also wrote a utility called jinjachecker, which helps troubleshoot the most common nesting problems.

Note: See TracWiki for help on using the wiki.