until nil {

Python templates on AppEngine Follow Ups

25 Aug 2010

Following yesterday’s experiments, some asked for a more comprehensive test including some more frameworks. Flask, Tipfy and the templating engine Jinja2.
It was my first experience with these, Flask got my attention, it seems like a slick microframework. Jinja2 was deceptive because of its limited template syntax way too far from Python. Tipfy didn’t really please me, but looking at its performance figures, I might now take a second look at this one.

Lets first take time to look at the new implementations. Getting a feeling on how they differs from the previous.

Flask with Jinja2 (Flask built-in):

from google.appengine.ext.webapp import util
from flask import Flask, render_template
app = Flask(__name__)

@app.route("/flask/<numA>/<numB>")
def hello(numA,numB):
    return render_template('flaskTestTemplate.html', nums=range(int(numA)), numB=numB)
    
def main():
    util.run_wsgi_app(app)

if __name__ == "__main__":
    main()
<html>
<body>
    <table>
        {% for i in nums %}
            <tr><td>{{i}}</td><td>x {{numB}}</td><td>= {{i|int * numB|int}}</td></tr>
        {% endfor %}
    </table>
</body>
</html>

Flask with Mako:

import os
from google.appengine.ext.webapp import util
from flask import Flask
from mako.template import Template
app = Flask(__name__)

@app.route("/flaskmako/<numA>/<numB>")
def hello(numA,numB):
    return Template(filename=os.path.join(
      os.path.dirname(__file__),"flaskTestMakoTemplate.html")).render(numA=numA,numB=numB)
    
def main():
    util.run_wsgi_app(app)

if __name__ == "__main__":
    main()
<html>
<body>
    <table>
        % for i in range(int(numA)):
            <tr><td>${i}</td><td>x ${numB}</td><td>= ${int(i) * int(numB)}</td></tr>
        % endfor
    </table>
</body>
</html>

webapp with Jinja2

import os
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from jinja2 import Environment, FileSystemLoader

class MainHandler(webapp.RequestHandler):
    def get(self,numA,numB): 
        self.response.out.write(
            Environment(loader=FileSystemLoader(
                os.path.join(
                    os.path.dirname(__file__),"templates/"))).get_template(
                        "gjinjaTestTemplate.html").render(nums=range(int(numA)),numB=numB))

application = webapp.WSGIApplication(
  [(r'/gjinja/(.*)/(.*)', MainHandler)],debug=False)

def main():
    util.run_wsgi_app(application)

if __name__ == '__main__':
    main()
<html>
<body>
    <table>
        {% for i in nums %}
            <tr><td>{{i}}</td><td>x {{numB}}</td><td>= {{i|int * numB|int}}</td></tr>
        {% endfor %}
    </table>
</body>
</html>

Bottle with Jinja2:

import os, bottle
from bottle import get
from google.appengine.ext.webapp import util
from jinja2 import Environment, FileSystemLoader

bottle.debug(False)
app = bottle.default_app()

@get('/bottlejinja/:numA/:numB')
def index(numA,numB):
  return Environment(loader=FileSystemLoader(
              os.path.join(
                  os.path.dirname(__file__),"templates/"))).get_template(
                      "bottleJinjaTestTemplate.html").render(nums=range(int(numA)),numB=numB)

def main():
  util.run_wsgi_app(app)

if __name__ == "__main__":
  main()
<html>
<body>
    <table>
        {% for i in nums %}
            <tr><td>{{i}}</td><td>x {{numB}}</td><td>= {{i|int * numB|int}}</td></tr>
        {% endfor %}
    </table>
</body>
</html>

web.py with Jinja2:

import os
from google.appengine.ext.webapp import util
import web
from jinja2 import Environment, FileSystemLoader

web.config.debug = False
urls = (
	'/webpyjinja/(.*)/(.*)',	'index'
)
app = web.application(urls, globals())

class index:
  def GET(self,numA,numB):
    return Environment(loader=FileSystemLoader(
                os.path.join(
                    os.path.dirname(__file__),"templates/"))).get_template(
                        "webpyJinjaTestTemplate.html").render(nums=range(int(numA)),numB=numB)

def main():
  util.run_wsgi_app(app.wsgifunc())

if __name__ == "__main__":
  main()
<html>
<body>
    <table>
        {% for i in nums %}
            <tr><td>{{i}}</td><td>x {{numB}}</td><td>= {{i|int * numB|int}}</td></tr>
        {% endfor %}
    </table>
</body>
</html>

tipfy with Jinja2:

import tipfy
from tipfy import RequestHandler, Rule
from tipfy.ext.jinja2 import render_response, default_config

default_config['templates_dir']='app/tipfy/templates'

class TipfyBenchmarkHandler(RequestHandler):
    def get(self,numA,numB):
        return render_response('tipfyJinjaTestTemplate.html', nums=range(numA), numB=numB)

app = tipfy.Tipfy(rules=[
  Rule('/tipfy/<int:numA>/<int:numB>', endpoint='tipfy', handler=TipfyBenchmarkHandler)])

def main():
    app.run()
    
if __name__ == '__main__':
    main()
<html>
<body>
    <table>
        {% for i in nums %}
            <tr><td>{{i}}</td><td>x {{numB}}</td><td>= {{i|int * numB|int}}</td></tr>
        {% endfor %}
    </table>
</body>
</html>

tipfy with Mako:

import os, tipfy
from tipfy import RequestHandler, Response, Rule
from mako.template import Template

class TipfyMakoBenchmarkHandler(RequestHandler):
    def get(self,numA,numB):
        return Response(
          Template(
            filename=os.path.join(
              os.path.dirname(__file__),
                "tipfyTestMakoTemplate.html")).render(numA=numA,numB=numB))

app = tipfy.Tipfy(rules=[
  Rule('/tipfymako/<int:numA>/<int:numB>', endpoint='tipfy', handler=TipfyMakoBenchmarkHandler)])

def main():
    app.run()
    
if __name__ == '__main__':
    main()
<html>
<body>
    <table>
        % for i in range(int(numA)):
            <tr><td>${i}</td><td>x ${numB}</td><td>= ${int(i) * int(numB)}</td></tr>
        % endfor
    </table>
</body>
</html>

And here is the test update wrapped inside a new consolidated benchmark. Also note that I modified all handlers to take advantage of AppEngine caching through the main() function.

Overhead 10:
   tipfy w/Jinja: | 1000 total, 0 failed |  88.376s overall, 0.088s each | score: 1.000 |
   flask w/Jinja: | 1000 total, 0 failed |  88.928s overall, 0.089s each | score: 1.006 |
   webapp w/Mako: | 1000 total, 0 failed |  91.340s overall, 0.091s each | score: 1.034 |
    tipfy w/Mako: | 1000 total, 0 failed |  91.624s overall, 0.092s each | score: 1.037 |
  webapp w/Jinja: | 1000 total, 0 failed |  91.951s overall, 0.092s each | score: 1.040 |
          bottle: | 1000 total, 0 failed |  98.956s overall, 0.099s each | score: 1.120 |
          web.py: | 1000 total, 0 failed |  99.725s overall, 0.100s each | score: 1.128 |
 webapp w/Django: | 1000 total, 0 failed | 100.619s overall, 0.101s each | score: 1.139 |
    flask w/Mako: | 1000 total, 0 failed | 103.797s overall, 0.104s each | score: 1.175 |
  web.py w/Jinja: | 1000 total, 0 failed | 103.985s overall, 0.104s each | score: 1.177 |
  bottle w/Jinja: | 1000 total, 0 failed | 104.074s overall, 0.104s each | score: 1.178 |
   bottle w/Mako: | 1000 total, 0 failed | 111.494s overall, 0.111s each | score: 1.262 |
   web.py w/Mako: | 1000 total, 0 failed | 115.740s overall, 0.116s each | score: 1.310 |


Overhead 100:
   tipfy w/Jinja: | 1000 total, 0 failed |  91.816s overall, 0.092s each | score: 1.000 |
   webapp w/Mako: | 1000 total, 0 failed |  94.623s overall, 0.095s each | score: 1.031 |
   bottle w/Mako: | 1000 total, 0 failed |  94.637s overall, 0.095s each | score: 1.031 |
    flask w/Mako: | 1000 total, 0 failed |  94.700s overall, 0.095s each | score: 1.031 |
  web.py w/Jinja: | 1000 total, 0 failed |  95.087s overall, 0.095s each | score: 1.036 |
          web.py: | 1000 total, 0 failed | 103.446s overall, 0.103s each | score: 1.127 |
   flask w/Jinja: | 1000 total, 0 failed | 104.981s overall, 0.105s each | score: 1.143 |
          bottle: | 1000 total, 0 failed | 105.155s overall, 0.105s each | score: 1.145 |
 webapp w/Django: | 1000 total, 0 failed | 107.200s overall, 0.107s each | score: 1.168 |
  webapp w/Jinja: | 1000 total, 0 failed | 107.458s overall, 0.107s each | score: 1.170 |
  bottle w/Jinja: | 1000 total, 0 failed | 107.681s overall, 0.108s each | score: 1.173 |
   web.py w/Mako: | 1000 total, 0 failed | 119.635s overall, 0.120s each | score: 1.303 |
    tipfy w/Mako: | 1000 total, 0 failed | 120.473s overall, 0.120s each | score: 1.312 |


Overhead 1000:
   tipfy w/Jinja: | 1000 total, 0 failed |  89.339s overall, 0.089s each | score: 1.000 |
          web.py: | 1000 total, 0 failed |  95.478s overall, 0.095s each | score: 1.069 |
  bottle w/Jinja: | 1000 total, 0 failed |  95.611s overall, 0.096s each | score: 1.070 |
  webapp w/Jinja: | 1000 total, 0 failed | 105.175s overall, 0.105s each | score: 1.177 |
  web.py w/Jinja: | 1000 total, 0 failed | 106.210s overall, 0.106s each | score: 1.189 |
   webapp w/Mako: | 1000 total, 0 failed | 106.407s overall, 0.106s each | score: 1.191 |
    flask w/Mako: | 1000 total, 0 failed | 106.724s overall, 0.107s each | score: 1.195 |
    tipfy w/Mako: | 1000 total, 0 failed | 107.081s overall, 0.107s each | score: 1.199 |
   web.py w/Mako: | 1000 total, 0 failed | 107.688s overall, 0.108s each | score: 1.205 |
          bottle: | 1000 total, 0 failed | 111.233s overall, 0.111s each | score: 1.245 |
   bottle w/Mako: | 1000 total, 0 failed | 117.832s overall, 0.118s each | score: 1.319 |
   flask w/Jinja: | 1000 total, 0 failed | 132.961s overall, 0.133s each | score: 1.488 |
 webapp w/Django: | 1000 total, 0 failed | 138.364s overall, 0.138s each | score: 1.549 |


Overhead 10000:
   tipfy w/Jinja: | 1000 total, 0 failed |  61.592s overall, 0.062s each | score: 1.000 |
    tipfy w/Mako: | 1000 total, 0 failed |  75.728s overall, 0.076s each | score: 1.230 |
   bottle w/Mako: | 1000 total, 0 failed |  81.219s overall, 0.081s each | score: 1.319 |
    flask w/Mako: | 1000 total, 0 failed |  81.409s overall, 0.081s each | score: 1.322 |
   web.py w/Mako: | 1000 total, 0 failed |  81.785s overall, 0.082s each | score: 1.328 |
   webapp w/Mako: | 1000 total, 0 failed |  82.661s overall, 0.083s each | score: 1.342 |
  webapp w/Jinja: | 1000 total, 0 failed |  84.205s overall, 0.084s each | score: 1.367 |
  bottle w/Jinja: | 1000 total, 0 failed |  84.636s overall, 0.085s each | score: 1.374 |
  web.py w/Jinja: | 1000 total, 0 failed |  85.994s overall, 0.086s each | score: 1.396 |
          web.py: | 1000 total, 0 failed | 126.019s overall, 0.126s each | score: 2.046 |
   flask w/Jinja: | 1000 total, 0 failed | 268.666s overall, 0.269s each | score: 4.362 |
          bottle: | 1000 total, 0 failed | 315.039s overall, 0.315s each | score: 5.115 |
 webapp w/Django: | 1000 total, 0 failed | 419.316s overall, 0.419s each | score: 6.808 |


Totals:
   tipfy w/Jinja: | 331.123 seconds total | score:   4.000 |
   webapp w/Mako: | 375.031 seconds total | score:   4.597 |
    flask w/Mako: | 386.630 seconds total | score:   4.722 |
  webapp w/Jinja: | 388.789 seconds total | score:   4.755 |
    tipfy w/Mako: | 394.907 seconds total | score:   4.777 |
  bottle w/Jinja: | 392.002 seconds total | score:   4.795 |
  web.py w/Jinja: | 391.276 seconds total | score:   4.797 |
   bottle w/Mako: | 405.181 seconds total | score:   4.930 |
   web.py w/Mako: | 424.849 seconds total | score:   5.146 |
          web.py: | 424.668 seconds total | score:   5.370 |
   flask w/Jinja: | 595.536 seconds total | score:   8.000 |
          bottle: | 630.383 seconds total | score:   8.625 |
 webapp w/Django: | 765.499 seconds total | score:  10.663 |

Take these results with a grain of salt… Especially since the difference margin is sow low. It’s in no way a full featured comparison!!!

UPDATE!! (2010-08-26) Tenjin and Brevé are also added in the next benchmark

blog comments powered by Disqus

}

Older Posts... Blog powered by Jekyll.
Built using Liquid, RedCloth, Pygments and Blueprint.

Copyright © 2008-2010 Louis-Philippe Perron