Browse Source

Pyramid benchmark for Python 2 and 3

ijl 11 years ago
parent
commit
ed8389150f

+ 6 - 0
pyramid/.gitignore

@@ -0,0 +1,6 @@
+.venv
+*.egg-info
+__pycache__
+*.pt.py
+*.egg
+.coverage

+ 2 - 0
pyramid/MANIFEST.in

@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include frameworkbenchmarks *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml

+ 31 - 0
pyramid/README.md

@@ -0,0 +1,31 @@
+# Pyramid benchmark test
+
+[Pyramid](http://www.pylonsproject.org/) is a flexible Python 2/3 framework.
+It uses [SQLAlchemy](http://www.sqlalchemy.org/) as its ORM, the default
+[Chameleon](http://www.pylonsproject.org/) for its templating, and
+[Gunicorn](https://github.com/benoitc/gunicorn) for the application server.
+
+## Test URLs
+
+### JSON Encoding
+
+http://localhost:6543/json
+
+### Single Row Random Query
+
+http://localhost:6543/db
+
+### Variable Row Query Test
+
+http://localhost:6543/queries?queries=10
+
+### Fortune Test
+
+http://localhost:6543/fortunes
+
+### Updates
+http://localhost:6543/updates?queries=10
+
+### Plaintext
+
+http://localhost:6543/plaintext

+ 51 - 0
pyramid/benchmark_config

@@ -0,0 +1,51 @@
+{
+  "framework": "pyramid",
+  "tests": [{
+    "py2": {
+      "setup_file": "setup_benchmark",
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 6543,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "PostgreSQL",
+      "framework": "Pyramid",
+      "language": "Python",
+      "orm": "Full",
+      "platform": "wsgi",
+      "webserver": "gunicorn",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "pyramid-py2",
+      "notes": "",
+      "versus": "wsgi"
+    }
+    "py3": {
+      "setup_file": "setup_benchmark",
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 6543,
+      "approach": "Realistic",
+      "classification": "Fullstack",
+      "database": "PostgreSQL",
+      "framework": "Pyramid",
+      "language": "Python",
+      "orm": "Full",
+      "platform": "wsgi",
+      "webserver": "gunicorn",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "pyramid-py3",
+      "notes": "",
+      "versus": "wsgi"
+    }
+  }]
+}

+ 37 - 0
pyramid/create_database.py

@@ -0,0 +1,37 @@
+from random import randint
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy import create_engine
+from frameworkbenchmarks.models import pg, DBSession, metadata, World, Fortune
+
+if __name__ == "__main__":
+    """
+    Initialize database
+    """
+    World.__table__.drop(pg, checkfirst=True)
+    Fortune.__table__.drop(pg, checkfirst=True)
+    DBSession.commit()
+    World.__table__.create(pg)
+    Fortune.__table__.create(pg)
+    DBSession.commit()
+    DBSession.execute(
+        World.__table__.insert([(num, randint(1, 10001)) for num in range(1, 10001)])
+    )
+    DBSession.execute(
+        Fortune.__table__.insert([
+            ("1", "fortune: No such file or directory"),
+            ("2", "A computer scientist is someone who fixes things that aren't broken."),
+            ("3", "After enough decimal places, nobody gives a damn."),
+            ("4", "A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1"),
+            ("5", "A computer program does what you tell it to do, not what you want it to do."),
+            ("6", "Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen"),
+            ("7", "Any program that runs right is obsolete."),
+            ("8", "A list is only as strong as its weakest link. — Donald Knuth"),
+            ("9", "Feature: A bug with seniority."),
+            ("10", "Computers make very fast, very accurate mistakes."),
+            ("11", """<script>alert("This should not be displayed in a browser alert box.");</script>"""),
+            ("12", "フレームワークのベンチマーク"),
+        ])
+    )
+    DBSession.commit()
+
+

+ 54 - 0
pyramid/development.ini

@@ -0,0 +1,54 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:FrameworkBenchmarks
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:gunicorn#main
+host = 0.0.0.0
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, frameworkbenchmarks
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = NOTSET
+handlers = console
+
+[logger_frameworkbenchmarks]
+level = NOTSET
+handlers =
+qualname = frameworkbenchmarks
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s

+ 23 - 0
pyramid/frameworkbenchmarks/__init__.py

@@ -0,0 +1,23 @@
+"""
+App config and initialization.
+"""
+
+from pyramid.request import Request
+from pyramid.config import Configurator
+from frameworkbenchmarks.models import sqlalchemy_encoder_factory
+
+
+def main(global_config, **settings):
+    """ This function returns a Pyramid WSGI application.
+    """
+    config = Configurator(settings=settings)
+    config.add_renderer('sqla_json', sqlalchemy_encoder_factory)
+    config.include('pyramid_chameleon')
+    config.add_route('test_1', '/json')
+    config.add_route('test_2', '/db')
+    config.add_route('test_3', '/queries')
+    config.add_route('test_4', '/fortunes')
+    config.add_route('test_5', '/updates')
+    config.add_route('test_6', '/plaintext')
+    config.scan()
+    return config.make_wsgi_app()

+ 67 - 0
pyramid/frameworkbenchmarks/models.py

@@ -0,0 +1,67 @@
+"""
+Benchmark models.
+"""
+
+import json
+import psycopg2
+from collections import Iterable
+from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.pool import QueuePool
+from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
+
+DBHOSTNAME = 'localhost'
+
+def get_conn():
+    return psycopg2.connect(
+        user = 'benchmarkdbuser',
+        password = 'benchmarkdbpass',
+        host = DBHOSTNAME,
+        port = '5432',
+        database = 'hello_world'
+        )
+
+conn_pool = QueuePool(get_conn, pool_size=100, max_overflow=25, echo=False)
+
+pg = create_engine('postgresql://', pool=conn_pool)
+DBSession = sessionmaker(bind=pg)()
+metadata = MetaData()
+
+DatabaseBase = declarative_base()
+
+def sqlalchemy_encoder_factory(system_values):
+    return SQLAlchemyEncoder()
+
+
+class SQLAlchemyEncoder(json.JSONEncoder):
+
+    def __call__(self, obj, system_values):
+        if isinstance(obj, Iterable):
+            return json.dumps([self.default(x) for x in obj])
+        else:
+            return json.dumps(self.default(obj))
+
+    def default(self, obj):
+        if isinstance(obj.__class__, DeclarativeMeta):
+            return obj.__json__()
+        return super(SQLAlchemyEncoder, self).default(obj)
+
+
+class World(DatabaseBase):
+    __tablename__ = 'world'
+
+    id = Column('id', Integer, primary_key=True)
+    randomNumber = Column('randomNumber', Integer)
+
+    def __json__(self):
+        return {'id': self.id, 'randomNumber': self.randomNumber}
+
+
+class Fortune(DatabaseBase):
+    __tablename__ = 'fortune'
+
+    id = Column('id', Integer, primary_key=True)
+    message = Column('message', String)
+
+    def __json__(self):
+        return {'id': self.id, 'message': self.message}

+ 18 - 0
pyramid/frameworkbenchmarks/templates/test_4.pt

@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Fortunes</title>
+</head>
+<body>
+    <table>
+    <tr>
+        <th>id</th>
+        <th>message</th>
+    </tr>
+    <tr tal:repeat="fortune fortunes">
+        <td>${fortune.id}</td>
+        <td>${fortune.message}</td>
+    </tr>
+    </table>
+</body>
+</html>

+ 166 - 0
pyramid/frameworkbenchmarks/tests.py

@@ -0,0 +1,166 @@
+# encoding: utf-8
+
+import unittest
+import json
+import sys
+
+class FunctionalTests(unittest.TestCase):
+    def setUp(self):
+        from frameworkbenchmarks import main
+        app = main({})
+        from webtest import TestApp
+        self.testapp = TestApp(app)
+        self.py3k = sys.version_info >= (3, 0)
+
+    def _get(self, url, content_type='application/json'):
+        res = self.testapp.get(url, status=200)
+        self.assertTrue('Content-Length' in res.headers)
+        # apparently these are all set by waitress, and so not
+        # available for testing here...
+        # self.assertTrue('Server' in res.headers)
+        # self.assertTrue('Date' in res.headers)
+        # self.assertTrue(content_type in res.headers['Content-Type'])
+        return res
+
+    def _str_compat(self, obj):
+        if self.py3k:
+            return obj.decode('utf-8')
+        return obj
+
+
+    def _test_obj(self, obj):
+        self.assertTrue('id' in obj)
+        self.assertTrue('randomNumber' in obj)
+        self.assertTrue(1 <= obj['randomNumber'] <= 10000)
+
+    def test_json(self):
+        """
+        /json
+        """
+        res = self._get('/json')
+        self.assertEqual(self._str_compat(res.body), """{"message": "Hello, World!"}""")
+
+    def test_db(self):
+        """
+        /db
+        """
+        res = self._get('/db')
+        obj = json.loads(self._str_compat(res.body))
+        self._test_obj(obj)
+
+    def test_queries_0(self):
+        """
+        /queries?queries=0
+        """
+        res = self._get('/queries?queries=0')
+        self.assertEqual(len(json.loads(self._str_compat(res.body))), 1)
+
+    def test_queries_999(self):
+        """
+        /queries?queries=999
+        """
+        res = self._get('/queries?queries=999')
+        self.assertEqual(len(json.loads(self._str_compat(res.body))), 500)
+
+    def test_queries_999(self):
+        """
+        /queries?queries=10 objects
+        """
+        res = self._get('/queries?queries=10')
+        objset = json.loads(self._str_compat(res.body))
+        for obj in objset:
+            self._test_obj(obj)
+
+    def test_fortunes(self):
+        """
+        /fortunes
+        """
+        res = self._get('/fortunes')
+        self.assertEqual(self._str_compat(res.body).strip(), fortunes.strip())
+
+    def test_updates(self):
+        """
+        /updates?queries=10
+        """
+        res = self._get('/updates?queries=10')
+        objset = json.loads(self._str_compat(res.body))
+        # don't bother with more...
+        for obj in objset:
+            self._test_obj(obj)
+
+    def test_plaintext(self):
+        """
+        /plaintext
+        """
+        res = self._get('/plaintext', content_type='text/plain')
+        self.assertEqual(self._str_compat(res.body), "Hello, World!")
+
+
+fortunes = """
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Fortunes</title>
+</head>
+<body>
+    <table>
+    <tr>
+        <th>id</th>
+        <th>message</th>
+    </tr>
+    <tr>
+        <td>11</td>
+        <td>&lt;script&gt;alert("This should not be displayed in a browser alert box.");&lt;/script&gt;</td>
+    </tr>
+    <tr>
+        <td>4</td>
+        <td>A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1</td>
+    </tr>
+    <tr>
+        <td>5</td>
+        <td>A computer program does what you tell it to do, not what you want it to do.</td>
+    </tr>
+    <tr>
+        <td>2</td>
+        <td>A computer scientist is someone who fixes things that aren't broken.</td>
+    </tr>
+    <tr>
+        <td>8</td>
+        <td>A list is only as strong as its weakest link. — Donald Knuth</td>
+    </tr>
+    <tr>
+        <td>0</td>
+        <td>Additional fortune added at request time.</td>
+    </tr>
+    <tr>
+        <td>3</td>
+        <td>After enough decimal places, nobody gives a damn.</td>
+    </tr>
+    <tr>
+        <td>7</td>
+        <td>Any program that runs right is obsolete.</td>
+    </tr>
+    <tr>
+        <td>10</td>
+        <td>Computers make very fast, very accurate mistakes.</td>
+    </tr>
+    <tr>
+        <td>6</td>
+        <td>Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen</td>
+    </tr>
+    <tr>
+        <td>9</td>
+        <td>Feature: A bug with seniority.</td>
+    </tr>
+    <tr>
+        <td>1</td>
+        <td>fortune: No such file or directory</td>
+    </tr>
+    <tr>
+        <td>12</td>
+        <td>フレームワークのベンチマーク</td>
+    </tr>
+    </table>
+</body>
+</html>
+"""

+ 97 - 0
pyramid/frameworkbenchmarks/views.py

@@ -0,0 +1,97 @@
+"""
+Test views, per the spec here:
+    http://www.techempower.com/benchmarks/#section=code&hw=i7&test=json
+"""
+
+from random import randint
+from pyramid.view import view_config
+from frameworkbenchmarks.models import DBSession, World, Fortune
+
+
+@view_config(route_name='test_1', renderer='json')
+def test_1(request):
+    """
+    Test type 1: JSON serialization
+    """
+    return {"message":"Hello, World!"}
+
+
+@view_config(route_name='test_2', renderer='sqla_json')
+def test_2(request):
+    """
+    Test type 2: Single database query
+    """
+    num = randint(1, 10001)
+    result = DBSession.query(World).filter(World.id == num).one()
+    return result
+
+
+@view_config(route_name='test_3', renderer='sqla_json')
+def test_3(request):
+    """
+    Test type 3: Multiple database queries
+    """
+    queries = request.params['queries']
+    try:
+        queries = int(queries)
+    except ValueError:
+        queries = 1
+    else:
+        if queries < 1:
+            queries = 1
+        elif queries > 500:
+            queries = 500
+    result = [
+        DBSession.query(World).filter(World.id == num).one()
+        for num in [randint(1, 10001) for _ in range(1, queries + 1)]
+    ]
+    return result
+
+
+@view_config(route_name='test_4', renderer='templates/test_4.pt')
+def test_4(request):
+    """
+    Test type 4: Fortunes
+    """
+    fortunes = [obj.__json__() for obj in DBSession.query(Fortune).all()]
+    fortunes.append(
+        {"id": 0, "message": "Additional fortune added at request time."}
+    )
+    return {'fortunes': sorted(fortunes, key=lambda x: x['message'])}
+
+
+@view_config(route_name='test_5', renderer='json')
+def test_5(request):
+    """
+    Test type 5: Database updates
+    """
+    queries = request.params['queries']
+    try:
+        queries = int(queries)
+    except ValueError:
+        queries = 1
+    else:
+        if queries < 1:
+            queries = 1
+        elif queries > 500:
+            queries = 500
+    objset = [
+        DBSession.query(World).filter(World.id == num).one()
+        for num in [randint(1, 10001) for _ in range(1, queries + 1)]
+    ]
+    for obj in objset:
+        obj.randomNumber = randint(1, 10001)
+    resultset = [obj.__json__() for obj in objset]
+    DBSession.commit()
+    return resultset
+
+
+@view_config(route_name='test_6')
+def test_6(request):
+    """
+    Test type 6: Plaintext
+    """
+    response = request.response
+    response.text = u"Hello, World!" # py2k/3k
+    response.content_type = "text/plain"
+    return response

+ 54 - 0
pyramid/production.ini

@@ -0,0 +1,54 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:FrameworkBenchmarks
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:gunicorn#main
+host = 0.0.0.0
+port = 6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, frameworkbenchmarks
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = NOTSET
+handlers = console
+
+[logger_frameworkbenchmarks]
+level = NOTSET
+handlers =
+qualname = frameworkbenchmarks
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s

+ 27 - 0
pyramid/setup.cfg

@@ -0,0 +1,27 @@
+[nosetests]
+match = ^test
+nocapture = 1
+cover-package = frameworkbenchmarks
+with-coverage = 1
+cover-erase = 1
+
+[compile_catalog]
+directory = frameworkbenchmarks/locale
+domain = frameworkbenchmarks
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = frameworkbenchmarks/locale/frameworkbenchmarks.pot
+width = 80
+
+[init_catalog]
+domain = frameworkbenchmarks
+input_file = frameworkbenchmarks/locale/frameworkbenchmarks.pot
+output_dir = frameworkbenchmarks/locale
+
+[update_catalog]
+domain = frameworkbenchmarks
+input_file = frameworkbenchmarks/locale/frameworkbenchmarks.pot
+output_dir = frameworkbenchmarks/locale
+previous = true

+ 41 - 0
pyramid/setup.py

@@ -0,0 +1,41 @@
+import os
+
+from setuptools import setup, find_packages
+
+requires = [
+    'pyramid',
+    'pyramid_chameleon',
+    'psycopg2',
+    'sqlalchemy',
+    'gunicorn'
+    ]
+
+tests_require = [
+  'nose-cov',
+  'webtest'
+]
+
+setup(name='frameworkbenchmarks',
+      version='0.0',
+      description='FrameworkBenchmarks',
+      classifiers=[
+        "Programming Language :: Python",
+        "Framework :: Pyramid",
+        "Topic :: Internet :: WWW/HTTP",
+        "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+        ],
+      author='',
+      author_email='',
+      url='',
+      keywords='web pyramid pylons',
+      packages=find_packages(),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=requires,
+      tests_require=tests_require,
+      test_suite="frameworkbenchmarks",
+      entry_points="""\
+      [paste.app_factory]
+      main = frameworkbenchmarks:main
+      """,
+      )

+ 34 - 0
pyramid/setup_benchmark.py

@@ -0,0 +1,34 @@
+import subprocess
+import setup_util
+import multiprocessing
+import os
+
+home = os.path.expanduser('~')
+bin_dir = os.path.expanduser('~/FrameworkBenchmarks/installs/py3/bin')
+NCPU = multiprocessing.cpu_count()
+
+proc = None
+
+
+def start(args):
+    global proc
+    setup_util.replace_text(
+        "frameworkbenchmarks/models.py",
+        "DBHOSTNAME = 'localhost'",
+        "DBHOSTNAME = '%s'" % args.database_host
+    )
+    proc = subprocess.Popen([
+        bin_dir + '/gunicorn',
+        'wsgi:app',
+        '-b', "0.0.0.0:6543",
+        '-w', str(NCPU*3)],
+        cwd='pyramid'
+    ])
+    return 0
+
+def stop():
+    global proc
+    if proc is not None:
+        proc.terminate()
+        proc.wait()
+    return 0

+ 3 - 0
pyramid/wsgi.py

@@ -0,0 +1,3 @@
+from paste.deploy import loadapp
+
+app = loadapp('config:production.ini', relative_to='.')