Tuesday, October 23, 2012

Python Templates Benchmark

Python template engines offer high reusability of markup code and the following features are used by content developers most of the time:
  • Includes: useful to incorporate some snippets of content that in most cases are common to the site, e.g. footer, scripts, styles, etc.
  • Extends: useful to define a master layout for the majority of the site content with placeholders, e.g. sidebar, horizontal menu, content, etc. The content developers extend the master layout by substituting available placeholders.
  • Widgets: usually small snippets of highly reusable markup, e.g. list item, button, etc. The content developers use widgets to increase readability and enforce consistency of design.
We will examine all mentioned features above. Test is executed in isolated environment using CPython 2.7. Latest available versions as of this writing:
  1. django 1.4.3
  2. jinja2 2.6
  3. mako 0.7.3
  4. tenjin 1.1.1
  5. tornado 2.4.1
  6. wheezy.template 0.1.132
Includes & Extends: in this test case an initial version of HTML content is refactored to use include and extend features of respective template engine.
Widgets: the test case is build around how widget is built and used.
02-single - a widget is built in a way that loop is inside; 03-loop - a widget represent an item that is rendered in a loop.
Here are raw numbers:
05-template

len(items) == 0

01-initial         msec    rps  tcalls  funcs
django            13666   7317     146     40
jinja2             3464  28867      26     20
mako               7811  12802      47     35
tenjin             2715  36831      27     21
tornado            2318  43135      28     17
wheezy.template     573 174441      14      8

02-include         msec    rps  tcalls  funcs
django            25101   3984     300     45
jinja2            14468   6912      98     36
mako              32303   3096     149     49
tenjin             9543  10479      88     27
tornado            2473  40441      35     17
wheezy.template    1239  80713      29     13

03-extends         msec    rps  tcalls  funcs
django            45856   2181     517     68
jinja2            17431   5737     149     42
mako              45842   2181     226     64
tenjin            13998   7144     133     38
tornado            2608  38350      40     17
wheezy.template    2249  44469      47     18

04-preprocess      msec    rps  tcalls  funcs
django                 not available
jinja2                 not available
mako                   not available
tenjin                 not available
tornado                not available
wheezy.template     572 174888      14      8

len(items) == 10

01-initial         msec    rps  tcalls  funcs
django           133621    748    1352     49
jinja2            12825   7797     117     20
mako              14348   6970     136     35
tenjin             6718  14886      46     21
tornado            9644  10369     179     17
wheezy.template    2535  39449      73      8

02-include         msec    rps  tcalls  funcs
django           144662    691    1506     53
jinja2            24081   4153     189     36
mako              38297   2611     238     49
tenjin            13657   7322     107     27
tornado            9801  10203     186     17
wheezy.template    3291  30382      88     13

03-extends         msec    rps  tcalls  funcs
django           169264    591    1723     72
jinja2            27726   3607     262     42
mako              52785   1894     315     64
tenjin            18122   5518     137     35
tornado            9825  10179     191     17
wheezy.template    4421  22622     106     18

04-preprocess      msec    rps  tcalls  funcs
django                 not available
jinja2                 not available
mako                   not available
tenjin                 not available
tornado                not available
wheezy.template    2484  40263      73      8


06-widgets

len(names) == 0

01-initial         msec    rps  tcalls  funcs
django             5093  19633      63     30
jinja2             2181  45841      16     14
mako               6211  16101      35     32
tenjin             2087  47922      24     20
tornado            1787  55969      16     14
wheezy.template     332 300797       8      7

02-single          msec    rps  tcalls  funcs
django             8925  11204     104     31
jinja2             5890  16977      38     33
mako              14460   6916      74     50
tenjin             4414  22655      45     24
tornado            5095  19629      45     27
wheezy.template     621 160939      14      9

03-loop            msec    rps  tcalls  funcs
django             5244  19071      63     30
jinja2             4534  22054      30     28
mako              13288   7526      65     49
tenjin             2110  47403      24     20
tornado            1615  61929      16     14
wheezy.template     408 244825       9      8

len(names) == 1

01-initial         msec    rps  tcalls  funcs
django            12625   7920     127     45
jinja2             3243  30836      21     18
mako               6900  14493      42     35
tenjin             2645  37802      26     21
tornado            2436  41052      28     17
wheezy.template     460 217192      12      8

02-single          msec    rps  tcalls  funcs
django            16400   6098     168     46
jinja2             7001  14284      43     38
mako              15181   6587      81     52
tenjin             4948  20208      47     25
tornado            6034  16572      57     30
wheezy.template     757 132115      18     10

03-loop            msec    rps  tcalls  funcs
django            16266   6148     168     46
jinja2             7063  14157      43     37
mako              15183   6586      81     52
tenjin             4859  20581      43     25
tornado            6060  16501      57     30
wheezy.template     766 130472      18     10

len(names) == 10

01-initial         msec    rps  tcalls  funcs
django            62323   1605     685     45
jinja2             8906  11229      66     18
mako              11950   8368     105     35
tenjin             5126  19507      44     21
tornado            7809  12806     136     17
wheezy.template    1498  66765      48      8

02-single          msec    rps  tcalls  funcs
django            67078   1491     726     46
jinja2            12128   8246      88     38
mako              20386   4905     144     52
tenjin             7380  13550      65     25
tornado           11465   8723     165     30
wheezy.template    1937  51633      54     10

03-loop            msec    rps  tcalls  funcs
django           106136    942    1095     46
jinja2            21863   4574     160     37
mako              26057   3838     225     52
tenjin            26474   3777     214     25
tornado           45056   2219     426     30
wheezy.template    3676  27203      99     10
msec - a total time taken in milliseconds, rps - requests processed per second, tcalls - total number of calls made by corresponding template engine, funcs - a number of unique functions used.

Setup and Run

Prerequisites to be able run this in a clean debian testing installation.
apt-get install make python-dev python-virtualenv \
    unzip
The source code is hosted on bitbucket, clone it into some directory and setup virtual environment (this will download all necessary package dependencies per framework listed above).
hg clone https://bitbucket.org/akorn/helloworld
cd helloworld
make env -sC 05-template
make env -sC 06-widgets
Note, you can run this benchmark using any version of python, including pypy. Here are make targets to use:
make env VERSION=3.3
make pypy
Once environment is ready, cd to benchmark directory and run:
env/bin/python benchmark.py
Environment Specification:
  • Intel Xeon CPU X3430 @ 2.40GHz x 4, Kernel 3.2.0-4-amd64
  • Debian Testing, Python 2.7.3
Python has a number of template engines. A trivial use case gives you an idea where particular template engine stands in terms of performance and internal effectivity.

9 comments:

  1. I followed your series of web components benchmarks but I'm not sure how relevant they are to the "real world". If you take the slowest template engine in the slowest test it is 596 RPS = about 2ms per request. And for a fast website using SQL it takes let's say 50ms (probably more) to render complete HTML response, with majority of time spent waiting for SQL server and logging to files.

    So even the slowest template engine takes only 4% of time, and you're optimizing this 4% part. I'm not saying it's useless to optimize these (for sure there are use cases and loads when it matters), only that for most people functionality is what matters much more.

    ReplyDelete
    Replies
    1. Why not put cache between web application and database? That changes use case dramatically. Don't you think?

      Delete
    2. I think when cached values are kilobytes in size then even memcached give >1ms latencies. Also a single uncached HDD access means at least 8-10ms latency (not necesserily SQL access, can be eg. logging). I would say that 50ms for computing server response for a real world website is very fast.

      Delete
    3. I would suggest take a look at C10K topic. The question is about efficiency.

      Delete
  2. Thank you for the nice benchmark, especially for handing out the source. It would be interesting to see how django templates perform if you activate the cached template loader (I assume it will be considerably faster):

    TEMPLATE_LOADERS = (
    ('django.template.loaders.cached.Loader', (
    'django.template.loaders.filesystem.Loader',
    'django.template.loaders.app_directories.Loader',
    )),
    )

    ReplyDelete
    Replies
    1. I believe this is how it used, actually. See the source:
      https://bitbucket.org/akorn/helloworld/src/tip/05-template/django/app.py

      Delete
  3. I always get annoyed by these "how relevant are your benchmarks on an application?" / "Use a cache". Well, template rendering IS a part of an application, and it's easier and more wise to benchmark isolated parts then the whole. Everyone knows what caching can do for a web application and that a web application is not just templating. He's not claiming application for application performance, but TEMPLATING. SO, thanks for the test, it is very relevant.

    By the way, not all applications using templates are web ones, and not every template is cacheable.

    ReplyDelete
  4. Nice article! It would be interesting if Tenjin were included in it.

    ReplyDelete
    Replies
    1. I have updated post with tenjin in the list now.

      Delete