From 9e50fd646e503b712c2ae5f5896b3a7a90e4718e Mon Sep 17 00:00:00 2001 From: Raymond Date: Sun, 12 Nov 2023 19:08:27 +0100 Subject: [PATCH 1/2] Add heaven framework to tfb --- frameworks/Python/heaven/README.md | 83 ++++++++++ frameworks/Python/heaven/app.py | 152 ++++++++++++++++++ .../Python/heaven/benchmark_config.json | 30 ++++ frameworks/Python/heaven/config.toml | 19 +++ frameworks/Python/heaven/heaven.dockerfile | 18 +++ frameworks/Python/heaven/heaven_conf.py | 12 ++ frameworks/Python/heaven/requirements.txt | 4 + .../Python/heaven/templates/fortune.html | 10 ++ 8 files changed, 328 insertions(+) create mode 100755 frameworks/Python/heaven/README.md create mode 100644 frameworks/Python/heaven/app.py create mode 100755 frameworks/Python/heaven/benchmark_config.json create mode 100644 frameworks/Python/heaven/config.toml create mode 100644 frameworks/Python/heaven/heaven.dockerfile create mode 100644 frameworks/Python/heaven/heaven_conf.py create mode 100644 frameworks/Python/heaven/requirements.txt create mode 100644 frameworks/Python/heaven/templates/fortune.html diff --git a/frameworks/Python/heaven/README.md b/frameworks/Python/heaven/README.md new file mode 100755 index 00000000000..8d4cb3eb09b --- /dev/null +++ b/frameworks/Python/heaven/README.md @@ -0,0 +1,83 @@ +# Congratulations! + +You have successfully built a new test in the suite! + +There are some remaining tasks to do before you are ready to open a pull request, however. + +## Next Steps + +1. Gather your source code. + +You will need to ensure that your source code is beneath this directory. The most common solution is to include a `src` directory and place your source code there. + +2. Edit `benchmark_config.json` + +You will need alter `benchmark_config.json` to have the appropriate end-points and port specified. + +3. Create `heaven.dockerfile` + +This is the dockerfile that is built into a docker image and run when a benchmark test is run. Specifically, this file tells the suite how to build and start your test application. + +You can create multiple implementations and they will all conform to `[name in benchmark_config.json].dockerfile`. For example, the `default` implementation in `benchmark_config.json` will be `heaven.dockerfile`, but if you wanted to make another implementation that did only the database tests for MySQL, you could make `heaven-mysql.dockerfile` and have an entry in your `benchmark_config.json` for `heaven-mysql`. + +4. Test your application + + $ tfb --mode verify --test heaven + +This will run the suite in `verify` mode for your test. This means that no benchmarks will be captured and we will test that we can hit your implementation end-points specified by `benchmark_config.json` and that the response is correct. + +Once you are able to successfully run your test through our suite in this way **and** your test passes our validation, you may move on to the next step. + +5. Add your test to `.github/workflows/build.yml` + +Edit `.github/workflows/build.yml` to ensure that Github Actions will automatically run our verification tests against your new test. This file is kept in alphabetical order, so find where `TESTDIR=Python/heaven` should be inserted under `env > matrix` and put it there. + +6. Fix this `README.md` and open a pull request + +Starting on line 49 is your actual `README.md` that will sit with your test implementation. Update all the dummy values to their correct values so that when people visit your test in our Github repository, they will be greated with information on how your test implementation works and where to look for useful source code. + +After you have the real `README.md` file in place, delete everything above line 59 and you are ready to open a pull request. + +Thanks and Cheers! + + + + +# Heaven Benchmarking Test + +This is the Heaven Web Framework implementation of a [benchmarking tests suite](../../) +comparing a variety of web development platforms. + +The information below is specific to Heaven Web Framework. 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 + +[**Heaven**](https://github.com/rayattack/heaven) is a simple, quick to learn, extremely fast (high-performance) framework for building Web Applications with Python 3.6+. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to ASGI). + +* **Fast to code**: Increase the speed to develop features by about 300% to 500% *. +* **Less bugs**: Reduce about 40% of human (developer) induced errors. * +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Less bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Loosely Opinionated**: You choose if you want to Pydantic, UJson or Orjson, SQLAlchemy or AsyncPG - Heaven get's out of your way *very fast*. + +* estimation based on tests on an internal development team, building production applications. + +## Test Paths & Sources + +All of the test implementations are located within a single file ([app.py](app.py)). + +All the tests are based on the ones for ASGI, as Heaven is an ASGI based framework. + + +## Resources + +* [Heaven source code on GitHub](https://github.com/rayattack/heaven) +* [Heaven website - documentation](https://rayattack.github.io/heaven/) diff --git a/frameworks/Python/heaven/app.py b/frameworks/Python/heaven/app.py new file mode 100644 index 00000000000..d3059ce83cb --- /dev/null +++ b/frameworks/Python/heaven/app.py @@ -0,0 +1,152 @@ +from multiprocessing import cpu_count, pool +from os import getenv +from random import randint, sample + +from asyncpg import create_pool +from heaven import Application, Context, Request, Response +from heaven.constants import STARTUP, SHUTDOWN +from orjson import dumps + + +############# +# constants # +############# +APPLICATION_JSON = 'application/json' +CONTENT_TYPE = 'Content-Type' +POOL = 'pool' +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//cpu_count() +MIN_POOL_SIZE = max(int(MAX_POOL_SIZE / 2), 1) + + +######################################### +# HOOKS for the app (shutdown, startup) # +######################################### +async def up_database(app: Application): + pool = await create_pool( + user=getenv("PGUSER", "benchmarkdbuser"), + password=getenv("PGPASS", "benchmarkdbpass"), + database="hello_world", + host="tfb-database", + port=5432, + min_size=MIN_POOL_SIZE, + max_size=MAX_POOL_SIZE, + ) + app.keep(POOL, pool) + + +async def down_database(app: Application): + pool = app.unkeep(POOL) + await pool.close() + + +################ +# Helper utils # +################ +def get_num_queries(queries): + try: query_count = int(queries) + except (ValueError, TypeError): return 1 + + if query_count < 1: return 1 + if query_count > 500: return 500 + return query_count + + +############################### +# Handlers for the app routes # +############################### +async def database(req: Request, res: Response, ctx: Context): + row_id = randint(1, 10000) + pool = req.app.peek(POOL) + async with pool.acquire() as connection: + number = await connection.fetchval(READ_ROW_SQL, row_id) + + res.headers = CONTENT_TYPE, APPLICATION_JSON + res.body = dumps({"id": row_id, "randomNumber": number}) + + +async def json(req: Request, res: Response, ctx: Context): + res.headers = CONTENT_TYPE, APPLICATION_JSON + res.body = dumps({'message': 'Hello, World!'}) + + +async def queries(req: Request, res: Response, ctx: Context): + pool = req.app.peek(POOL) + queries = req.params.get('queries') + num_queries = get_num_queries(queries) + row_ids = sample(range(1, 10001), num_queries) + worlds = [] + + async with pool.acquire() as connection: + statement = await connection.prepare(READ_ROW_SQL) + for row_id in row_ids: + number = await statement.fetchval(row_id) + worlds.append({"id": row_id, "randomNumber": number}) + + res.headers = CONTENT_TYPE, APPLICATION_JSON + res.body = dumps(worlds) + + +async def fortunes(req: Request, res: Response, ctx: Context): + pool = req.app.peek(POOL) + async with pool.acquire() as connection: + fortunes = await connection.fetch("SELECT * FROM Fortune") + + fortunes.append(ADDITIONAL_ROW) + fortunes.sort(key=lambda row: row[1]) + await res.render("fortune.html", fortunes=fortunes, request=req) + + +async def updates(req: Request, res: Response, ctx: Context): + pool = req.app.peek(POOL) + queries = req.params.get('queries') + num_queries = get_num_queries(queries) + + # To avoid deadlock + ids = sorted(sample(range(1, 10000 + 1), num_queries)) + numbers = sorted(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 pool.acquire() as connection: + statement = await connection.prepare(READ_ROW_SQL) + for row_id, _ in updates: + await statement.fetchval(row_id) + await connection.executemany(WRITE_ROW_SQL, updates) + res.headers = CONTENT_TYPE, APPLICATION_JSON + res.body = dumps(worlds) + + +async def plaintext(req: Request, res: Response, ctx: Context): + res.headers = 'Content-Type', 'text/plain' + res.body = b"Hello, World!" + + +################ +# App creation # +################ +app = Application() +app.TEMPLATES('templates') + + +################### +# Register hooks # +################### +app.ON(STARTUP, up_database) +app.ON(SHUTDOWN, down_database) + + +################### +# Register routes # +################### +app.GET('/db', database) +app.GET('/queries', queries) +app.GET('/fortunes', fortunes) +app.GET('/updates', updates) +app.GET('/plaintext', plaintext) +app.GET('/json', json) diff --git a/frameworks/Python/heaven/benchmark_config.json b/frameworks/Python/heaven/benchmark_config.json new file mode 100755 index 00000000000..177595b3ad7 --- /dev/null +++ b/frameworks/Python/heaven/benchmark_config.json @@ -0,0 +1,30 @@ +{ + "framework": "heaven", + "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": "Heaven", + "language": "Python", + "flavor": "Python3", + "platform": "None", + "webserver": "None", + "os": "Linux", + "orm": "Raw", + "database_os": "Linux", + "database": "Postgres", + "display_name": "Heaven", + "versus": "None", + "notes": "" + } + } + ] +} diff --git a/frameworks/Python/heaven/config.toml b/frameworks/Python/heaven/config.toml new file mode 100644 index 00000000000..e67796dca5f --- /dev/null +++ b/frameworks/Python/heaven/config.toml @@ -0,0 +1,19 @@ +[framework] +name = "heaven" + +[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 = "None" +webserver = "None" +versus = "None" diff --git a/frameworks/Python/heaven/heaven.dockerfile b/frameworks/Python/heaven/heaven.dockerfile new file mode 100644 index 00000000000..ff1b711f937 --- /dev/null +++ b/frameworks/Python/heaven/heaven.dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11 + +WORKDIR /heaven + +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +RUN pip3 install cython==0.29.36 + +COPY requirements.txt ./ + +RUN pip3 install -r requirements.txt + +COPY . ./ + +EXPOSE 8080 + +CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c heaven_conf.py diff --git a/frameworks/Python/heaven/heaven_conf.py b/frameworks/Python/heaven/heaven_conf.py new file mode 100644 index 00000000000..8ffea8153ba --- /dev/null +++ b/frameworks/Python/heaven/heaven_conf.py @@ -0,0 +1,12 @@ +import multiprocessing +import os + +_is_travis = os.environ.get('TRAVIS') == 'true' + +workers = multiprocessing.cpu_count() + +bind = "0.0.0.0:8080" +keepalive = 120 +errorlog = '-' +pidfile = '/tmp/heaven.pid' +loglevel = 'error' diff --git a/frameworks/Python/heaven/requirements.txt b/frameworks/Python/heaven/requirements.txt new file mode 100644 index 00000000000..3e8a09ee6f7 --- /dev/null +++ b/frameworks/Python/heaven/requirements.txt @@ -0,0 +1,4 @@ +asyncpg==0.29.0 +heaven==0.2.4 +orjson==3.9.10 +gunicorn==20.1.0 diff --git a/frameworks/Python/heaven/templates/fortune.html b/frameworks/Python/heaven/templates/fortune.html new file mode 100644 index 00000000000..1c90834285d --- /dev/null +++ b/frameworks/Python/heaven/templates/fortune.html @@ -0,0 +1,10 @@ + + +Fortunes + + + +{% for fortune in fortunes %} +{% endfor %}
idmessage
{{ fortune[0] }}{{ fortune[1]|e }}
+ + From d3c8b7eb3e8cd421dba8d2a93bb59f71fac28815 Mon Sep 17 00:00:00 2001 From: Raymond Date: Sun, 12 Nov 2023 19:21:40 +0100 Subject: [PATCH 2/2] Add Heaven Web Framework: update documentation after running all tests --- frameworks/Python/heaven/README.md | 45 ------------------------------ 1 file changed, 45 deletions(-) diff --git a/frameworks/Python/heaven/README.md b/frameworks/Python/heaven/README.md index 8d4cb3eb09b..a62ff905719 100755 --- a/frameworks/Python/heaven/README.md +++ b/frameworks/Python/heaven/README.md @@ -1,48 +1,3 @@ -# Congratulations! - -You have successfully built a new test in the suite! - -There are some remaining tasks to do before you are ready to open a pull request, however. - -## Next Steps - -1. Gather your source code. - -You will need to ensure that your source code is beneath this directory. The most common solution is to include a `src` directory and place your source code there. - -2. Edit `benchmark_config.json` - -You will need alter `benchmark_config.json` to have the appropriate end-points and port specified. - -3. Create `heaven.dockerfile` - -This is the dockerfile that is built into a docker image and run when a benchmark test is run. Specifically, this file tells the suite how to build and start your test application. - -You can create multiple implementations and they will all conform to `[name in benchmark_config.json].dockerfile`. For example, the `default` implementation in `benchmark_config.json` will be `heaven.dockerfile`, but if you wanted to make another implementation that did only the database tests for MySQL, you could make `heaven-mysql.dockerfile` and have an entry in your `benchmark_config.json` for `heaven-mysql`. - -4. Test your application - - $ tfb --mode verify --test heaven - -This will run the suite in `verify` mode for your test. This means that no benchmarks will be captured and we will test that we can hit your implementation end-points specified by `benchmark_config.json` and that the response is correct. - -Once you are able to successfully run your test through our suite in this way **and** your test passes our validation, you may move on to the next step. - -5. Add your test to `.github/workflows/build.yml` - -Edit `.github/workflows/build.yml` to ensure that Github Actions will automatically run our verification tests against your new test. This file is kept in alphabetical order, so find where `TESTDIR=Python/heaven` should be inserted under `env > matrix` and put it there. - -6. Fix this `README.md` and open a pull request - -Starting on line 49 is your actual `README.md` that will sit with your test implementation. Update all the dummy values to their correct values so that when people visit your test in our Github repository, they will be greated with information on how your test implementation works and where to look for useful source code. - -After you have the real `README.md` file in place, delete everything above line 59 and you are ready to open a pull request. - -Thanks and Cheers! - - - - # Heaven Benchmarking Test This is the Heaven Web Framework implementation of a [benchmarking tests suite](../../)