Explorar el Código

[Python] Add BlackSheep framework (#8164)

Oleg S hace 2 años
padre
commit
3dc8124a7f

+ 38 - 0
frameworks/Python/blacksheep/README.md

@@ -0,0 +1,38 @@
+# BlackSheep Benchmark Test
+
+This is the BlackSheep portion of a [benchmarking tests suite](../../)
+comparing a variety of web development platforms.
+
+The information below is specific to BlackSheep. For further guidance,
+review the [documentation](https://github.com/TechEmpower/FrameworkBenchmarks/wiki).
+Also note that there is additional information provided in
+the [Python README](../).
+
+## Description
+
+[BlackSheep](https://github.com/Neoteroi/BlackSheep) is a fast HTTP Server/Client microframework for Python [asyncio](https://docs.python.org/3/library/asyncio.html), using [Cython](https://cython.org), 
+[`uvloop`](https://magic.io/blog/uvloop-blazing-fast-python-networking/), and 
+[`httptools`](https://github.com/MagicStack/httptools). 
+
+<p align="left">
+  <a href="#blacksheep"><img width="320" height="271" src="https://raw.githubusercontent.com/Neoteroi/BlackSheep/master/black-sheep.svg?sanitize=true" alt="Black Sheep"></a>
+</p>
+
+
+## Implementation
+
+BlackSheep is implemented using:
+
+* [asyncio](https://docs.python.org/3/library/asyncio.html).
+* [Cython](https://cython.org)
+* [`uvloop`](https://magic.io/blog/uvloop-blazing-fast-python-networking/).
+* [`httptools`](https://github.com/MagicStack/httptools).
+* Python built-in multiprocessing module.
+
+## Test Paths & Sources
+
+All of the test implementations are located within a single file ([app.py](app.py)).
+
+## Resources
+
+* [Repo](https://github.com/Neoteroi/BlackSheep)

+ 220 - 0
frameworks/Python/blacksheep/app.py

@@ -0,0 +1,220 @@
+import os
+import ujson
+import asyncpg
+import multiprocessing
+import random
+import blacksheep as bs
+import jinja2
+from email.utils import formatdate
+
+try:
+    from ujson import dumps as jsonify
+except:
+    from json import dumps as jsonify
+
+
+_is_travis = os.environ.get('TRAVIS') == 'true'
+
+_is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
+
+_cpu_count = multiprocessing.cpu_count()
+if _is_travis:
+    _cpu_count = 2
+
+
+#from blacksheep.settings.json import json_settings
+#json_settings.use(dumps=jsonify)
+
+DBDRV  = "postgres"
+DBHOST = "tfb-database"
+DBUSER = "benchmarkdbuser"
+DBPSWD = "benchmarkdbpass"
+
+READ_ROW_SQL = 'SELECT "id", "randomnumber" FROM "world" WHERE id = $1'
+WRITE_ROW_SQL = 'UPDATE "world" SET "randomnumber"=$1 WHERE id=$2'
+ADDITIONAL_ROW = [0, "Additional fortune added at request time."]
+MAX_POOL_SIZE = 1000 // multiprocessing.cpu_count()
+MIN_POOL_SIZE = max(int(MAX_POOL_SIZE / 2), 1)
+
+db_pool = None
+
+g_response_server = None
+g_response_add_date = False
+
+
+async def setup_db(app):
+    global db_pool
+    db_pool = await asyncpg.create_pool(
+        user=os.getenv('PGUSER', DBUSER),
+        password=os.getenv('PGPASS', DBPSWD),
+        database='hello_world',
+        host=DBHOST,
+        port=5432,
+        min_size=MIN_POOL_SIZE,
+        max_size=MAX_POOL_SIZE,        
+    )
+
+
+def load_fortunes_template():
+    path = os.path.join('templates', 'fortune.html')
+    with open(path, 'r') as template_file:
+        template_text = template_file.read()
+        return jinja2.Template(template_text)
+
+
+fortune_template = load_fortunes_template()
+
+app = bs.Application()
+app.on_start += setup_db
+
+
+def get_num_queries(request):
+    try:
+        value = request.query.get('queries')
+        if value is None:
+            return 1
+        query_count = int(value[0])
+    except (KeyError, IndexError, ValueError):
+        return 1
+    if query_count < 1:
+        return 1
+    if query_count > 500:
+        return 500
+    return query_count
+
+
+# ------------------------------------------------------------------------------------------
+
+async def bs_middleware(request, handler):
+    global g_response_server, g_response_add_date
+    response = await handler(request)
+    if g_response_server:
+        response.headers[b'Server'] = g_response_server
+    if g_response_add_date:
+        response.headers[b'Date'] = formatdate(timeval=None, localtime=False, usegmt=True)            
+    return response
+
+
[email protected]('/json')
+async def json_test(request):
+    return bs.json( {'message': 'Hello, world!'} )
+
+
[email protected]('/db')
+async def single_db_query_test(request):
+    row_id = random.randint(1, 10000)
+    
+    async with db_pool.acquire() as db_conn:
+        number = await db_conn.fetchval(READ_ROW_SQL, row_id)
+    
+    world = {'id': row_id, 'randomNumber': number}
+    return bs.json(world)
+
+
[email protected]('/queries')
+async def multiple_db_queries_test(request):
+    num_queries = get_num_queries(request)
+    row_ids = random.sample(range(1, 10000), num_queries)
+    worlds = [ ]
+
+    async with db_pool.acquire() as db_conn:
+        statement = await db_conn.prepare(READ_ROW_SQL)
+        for row_id in row_ids:
+            number = await statement.fetchval(row_id)
+            worlds.append( {"id": row_id, "randomNumber": number} )
+
+    return bs.json(worlds)
+
+
[email protected]('/fortunes')
+async def fortunes_test(request):
+    async with db_pool.acquire() as db_conn:
+        fortunes = await db_conn.fetch("SELECT * FROM Fortune")
+
+    fortunes.append(ADDITIONAL_ROW)
+    fortunes.sort(key = lambda row: row[1])
+    data = fortune_template.render(fortunes=fortunes)
+    return bs.html(data)
+
+
[email protected]('/updates')
+async def db_updates_test(request):
+    num_queries = get_num_queries(request)
+    ids = sorted(random.sample(range(1, 10000 + 1), num_queries))
+    numbers = sorted(random.sample(range(1, 10000), num_queries))
+    updates = list(zip(ids, numbers))
+
+    worlds = [ {"id": row_id, "randomNumber": number} for row_id, number in updates ]
+
+    async with db_pool.acquire() as db_conn:
+        statement = await db_conn.prepare(READ_ROW_SQL)
+        for row_id, _ in updates:
+            await statement.fetchval(row_id)
+        await db_conn.executemany(WRITE_ROW_SQL, updates)
+ 
+    return bs.json(worlds)
+
+
[email protected]('/plaintext')
+async def plaintext_test(request):
+    return bs.Response(200, content=bs.Content(b"text/plain", b'Hello, World!'))
+    #return bs.text('Hello, World!')
+
+
+# -----------------------------------------------------------------------------------
+
+if __name__ == "__main__":
+    import optparse
+    import logging
+    import re
+
+    parser = optparse.OptionParser("usage: %prog [options]", add_help_option=False)
+    parser.add_option("-h", "--host", dest="host", default='0.0.0.0', type="string")
+    parser.add_option("-p", "--port", dest="port", default=8080, type="int")
+    parser.add_option("-s", "--server", dest="server", default="uvicorn", type="string")
+    parser.add_option("-w", "--workers", dest="workers", default=0, type="int")
+    parser.add_option("-k", "--keepalive", dest="keepalive", default=60, type="int")
+    parser.add_option("-v", "--verbose", dest="verbose", default=0, type="int")
+    (opt, args) = parser.parse_args() 
+
+    workers = _cpu_count
+    if workers > 0:
+        workers = opt.workers
+
+    if _is_travis:
+        workers = 2
+
+    def run_app():
+        global g_response_server, g_response_add_date
+
+        if opt.gateway == "uvicorn":
+            import uvicorn
+            log_level = logging.ERROR
+            uvicorn.run(app, host=opt.host, port=opt.port, workers=1, loop="uvloop", log_level=log_level, access_log=False)
+        
+        if opt.server == 'fastwsgi':
+            import fastwsgi
+            from blacksheep.utils.aio import get_running_loop
+            g_response_server = b'FastWSGI'
+            app.middlewares.append(bs_middleware)
+            loop = get_running_loop()
+            loop.run_until_complete(app.start())
+            fastwsgi.run(app, host=opt.host, port=opt.port, loglevel=opt.verbose)
+
+        if opt.server == 'socketify':
+            import socketify
+            msg = "Listening on http://0.0.0.0:{port} now\n".format(port=opt.port)
+            socketify.WSGI(app).listen(opt.port, lambda config: logging.info(msg)).run()
+
+    def create_fork():
+        n = os.fork()
+        # n greater than 0 means parent process
+        if not n > 0:
+            run_app()
+
+    # fork limiting the cpu count - 1
+    for i in range(1, workers):
+        create_fork()
+
+    run_app()  # run app on the main process too :)
+

+ 28 - 0
frameworks/Python/blacksheep/benchmark_config.json

@@ -0,0 +1,28 @@
+{
+  "framework": "blacksheep",
+  "tests": [{
+    "default": {
+      "json_url": "/json",
+      "fortune_url": "/fortunes",
+      "plaintext_url": "/plaintext",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "update_url": "/updates?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "framework": "blacksheep",
+      "language": "Python",
+      "flavor": "Python3",
+      "platform": "ASGI",
+      "webserver": "uvicorn",
+      "os": "Linux",
+      "orm": "Raw",
+      "database_os": "Linux",
+      "database": "Postgres",
+      "display_name": "blacksheep",
+      "versus": "None",      
+      "notes": ""
+    }
+  }]
+}

+ 15 - 0
frameworks/Python/blacksheep/blacksheep.dockerfile

@@ -0,0 +1,15 @@
+FROM python:3.11-bullseye
+
+WORKDIR /blacksheep
+
+COPY ./ /blacksheep
+
+RUN pip3 install -U pip
+RUN pip3 install cython==0.29.34
+RUN pip3 install -r /blacksheep/requirements.txt
+RUN pip3 install -r /blacksheep/requirements-gunicorn.txt
+RUN pip3 install -r /blacksheep/requirements-uvicorn.txt
+
+EXPOSE 8080
+
+CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c blacksheep_conf.py

+ 14 - 0
frameworks/Python/blacksheep/blacksheep_conf.py

@@ -0,0 +1,14 @@
+import multiprocessing
+import os
+
+_is_travis = os.environ.get('TRAVIS') == 'true'
+
+workers = multiprocessing.cpu_count()
+if _is_travis:
+    workers = 2
+
+bind = "0.0.0.0:8080"
+keepalive = 120
+errorlog = '-'
+pidfile = '/tmp/blacksheep.pid'
+loglevel = 'error'

+ 19 - 0
frameworks/Python/blacksheep/config.toml

@@ -0,0 +1,19 @@
+[framework]
+name = "blacksheep"
+
+[main]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Platform"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "ASGI"
+webserver = "uvicorn"
+versus = "None"

+ 1 - 0
frameworks/Python/blacksheep/requirements-gunicorn.txt

@@ -0,0 +1 @@
+gunicorn==20.1.0

+ 1 - 0
frameworks/Python/blacksheep/requirements-hypercorn.txt

@@ -0,0 +1 @@
+hypercorn==0.14.3

+ 3 - 0
frameworks/Python/blacksheep/requirements-uvicorn.txt

@@ -0,0 +1,3 @@
+uvloop==0.17.0
+uvicorn==0.21.1
+httptools==0.5.0

+ 4 - 0
frameworks/Python/blacksheep/requirements.txt

@@ -0,0 +1,4 @@
+asyncpg==0.27.0
+Jinja2==3.1.2
+blacksheep==1.2.13
+ujson==5.7.0

+ 10 - 0
frameworks/Python/blacksheep/templates/fortune.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+<tr><th>id</th><th>message</th></tr>
+{% for fortune in fortunes %}<tr><td>{{ fortune[0] }}</td><td>{{ fortune[1]|e }}</td></tr>
+{% endfor %}</table>
+</body>
+</html>