10/1/11

Performance of Flask, Tornado, GEvent, and their combinations

When choosing a web framework, I pretty much have eyes set on Tornado. But I heard good things about Flask and Gevent. So I tested the performance of each and combinations of the three. I chose something just a little more advanced than a "Hello World" program to write - one that use templates. Here are the codes:

1, Pure Flask (pure_flask.py)
from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def main_handler():
   return render_template('main_j2.html', messages="whatever",title="home")

if __name__ == '__main__':
   app.run(port=8888, debug=False)

2, Pure Tornado (pure_tornado.py)
import os.path
import tornado.httpserver
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
   def get(self):
      self.render('main.html', page_title="", body_id="", messages="whatever",title="home")

settings = {
   "static_path":os.path.join(os.path.dirname(__file__),'static'),
   "template_path":"templates",
}
application = tornado.web.Application([
   (r"/", MainHandler),
], **settings)

if __name__ == "__main__":
   application.listen(8888)
   tornado.ioloop.IOLoop.instance().start()

3, Flask with Gevent (gevent_flask.py)
from gevent.wsgi import WSGIServer
from pure_flask import app

http_server = WSGIServer(('', 8888), app)
http_server.serve_forever()

4, Flask with Tornado (tornado_flask.py)
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from pure_flask import app

http_server = HTTPServer(WSGIContainer(app))
http_server.listen(8888)
IOLoop.instance().start()

5, Tornado with Gevent (gevent_tornado.py)
import tornado.wsgi
import gevent.wsgi
import pure_tornado

application = tornado.wsgi.WSGIApplication([
   (r"/", pure_tornado.MainHandler),
],**pure_tornado.settings)

if __name__ == "__main__":
   server = gevent.wsgi.WSGIServer(('', 8888), application)
   server.serve_forever()


I have 3 template files: main.html, layout.html, and form.html. main.html "extends" layout.html and "includes" form.html. The total size of templates is about 30kB.
The reason Flask and Tornado use different templates (main.html and main_j2.html) is that their template syntax is slightly different. Flask (jinja2) template use "{% endblock %}", "{% endfor %}", etc. while Tornado just use "{% end %}".

I tested performance (requests per second) using ApacheBench:
ab -n 1000 -c 4 http://localhost:8888/
and run it 5 times. The testing is done on a 6-year old dual-Opteron 254 server.

Here are the results:
pure_flask: 82 88 107 102 71
pure_tornado: 144 244 241 294 290
gevent_flask: 127 139 145 152 110
tornado_flask: 110 88 74 92 101
gevent_tornado: 328 555 177 273 153

Here are the averages:
pure_flask: 90
pure_tornado: 242
gevent_flask: 135
tornado_flask: 93
gevent_tornado: 297


As you can see, the Tornado implementation is significantly faster than Flask. Gevent makes Tornado faster, but not by a lot.

In the end, I like the straight-forward style of Tornado and not the Flask way to write large project (using blueprints), So I sticks with Tornado.

3/29/11

bytearray error (an integer is required)

If you see this error: "TypeError: an integer is required" when you use bytearray, it means you can't use string for bytearray anymore.

Bytearray was introduced in python 3.0, it was back-ported to 2.6, in Python 2.6 and 3.0, you can use string:
#in 2.6 or 3.0

>>> a = bytearray()
>>> a.extend("hello")
>>> a
bytearray(b'hello')

But in Python 3.2, it won't work anymore, you have to use list of integers:

>>> a = bytearray()
>>> a.extend("hello")
Traceback (most recent call last):
File "", line 1, in
TypeError: an integer is required
>>> a.extend(b'hello') #this is OK
#if you have to use a string, use str.encode()
>>> a.extend("hello".encode()) # or list(map(ord,"hello")) or [ord(e) for e in "hello"]

3/28/11

Command line IDN punycode convertor

If you need a command line punycode converter for IDN (internationalized domain name), just write a small Perl program using Net-LibIDN(cpan).

On Fedora, do a "yum install perl-Net-LibIDN" to install Net-LibIDN.

To encode:
$cat en-puny.pl
#!/bin/env perl
use Net::LibIDN ':all';
print idn_to_ascii($ARGV[0],'utf-8') . "\n";

To decode:
$cat de-puny.pl
#!/bin/env perl
use Net::LibIDN ':all';
print idn_to_unicode($ARGV[0],'utf-8') . "\n";

Examples:
$./en-puny.pl 你我.中国
xn--6qqp96b.xn--fiqs8s

./de-puny.pl xn--6qqp96b.xn--fiqs8s
你我.中国

$./en-puny.pl ˆ.net
xn--wqa.net

To get punycode without "xn--" prefix, replace idn_to_ascii with idn_punycode_encode, and replace idn_to_unicode with idn_punycode_decode.

2/17/11

Touch: PHP vs Linux

Linux's touch (in coreutils) opens and closes the file.
PHP's touch uses the 'utime' system call to change access/modify time of the file inode.

I was using inotify to watch a file and was surprised to find when PHP 'touch'es the file, nothing happened. After some digging I found the two 'touch'es are different.