Skip to content

Commit

Permalink
Enhance performance, asyncio handling, and documentation (#57)
Browse files Browse the repository at this point in the history
* Nothing

* Use orjson instead of json for speed improvement

* Increase keep alive timeout, sigificantly lowered timeout errors

* Reduce asyncio wait for overhead

* Better asyncio error handling

* Nvm

* Integrate logger with HttpServer

* Update readme

* Add Python 3.12 test run

* Fix pyproject.toml

* Cleanup requirements

* Test fix
  • Loading branch information
JoshCap20 authored Sep 25, 2024
1 parent b081e86 commit 3dade6c
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 83 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4
Expand Down
161 changes: 105 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,95 +10,144 @@ We designed Areion to have as few dependencies as possible. We created our own H

**Development Mode:** Add the flag `with_development_mode(True)` to the `AreionServerBuilder` to enable development mode. This mode will automatically add Swagger UI and OpenAPI routes to your server. They are accessible from the routes `/docs` and `/openapi` respectively.

## Table of Contents

- [Benchmark](#benchmark)
- [Benchmark Results](#benchmark-results)
- [Summary](#summary)
- [Visualization](#visualization)
- [Detailed Results](#detailed-results)
- [Analysis](#analysis)
- [Throughput (Requests per Second)](#throughput-requests-per-second)
- [Average Latency](#average-latency)
- [Total Requests Handled](#total-requests-handled)
- [Socket Errors](#socket-errors)
- [Getting Started](#getting-started)
- [Installation](#installation)
- [Quick Start Guide](#quick-start-guide)
- [Development Tools](#development-tools)
- [Core Components](#core-components)
- [AreionServer](#areionserver)
- [Router](#router)
- [HttpServer](#httpserver)
- [Default Component Implementation](#default-component-implementation)
- [Orchestrator](#orchestrator)
- [Logger](#logger)
- [Engine](#engine)
- [Advanced Usage](#advanced-usage)
- [Middleware](#middleware)
- [Grouping Routes](#grouping-routes)
- [Template Rendering](#template-rendering)
- [Task Scheduling](#task-scheduling)
- [API Reference](#api-reference)
- [AreionServer API](#areionserver-api)
- [Router API](#router-api)
- [HttpRequest and HttpResponse](#httprequest-and-httpresponse)
- [Exception Handling](#exception-handling)
- [Contributing](#contributing)
- [License](#license)

---

## Benchmark

We conducted performance benchmarks to compare Areion with FastAPI, focusing on throughput and latency under high-load conditions. The goal was to evaluate Areion's ability to handle concurrent connections efficiently and provide fast response times. We used the same JSON response in all frameworks to ensure a fair comparison.
We conducted performance benchmarks to compare **Areion**, **FastAPI**, and **Flask**, focusing on throughput and latency under high-load conditions. The goal was to evaluate each framework's ability to handle concurrent connections efficiently and provide fast response times. We used the same JSON response in all frameworks to ensure a fair comparison.

### Benchmark Results

These show the results of running the benchmark test for 30 seconds with 12 threads and 400 connections on my local machine. The test was conducted using the `wrk` benchmarking tool. The results are summarized below, followed by detailed output for each framework.

#### Summary

| Framework | Requests/sec | Avg Latency (ms) | Transfer/sec | Total Requests | Socket Errors |
| --------- | ------------ | ---------------- | ------------ | -------------- | ------------------------- |
| Areion | 47,241.97 | 8.46 | 4.42 MB | 1,418,550 | Read: 545 |
| FastAPI | 3,579.10 | 111.53 | 531.27 KB | 107,613 | Read: 419 |
| Flask | 555.98 | 47.45 | 104.79 KB | 16,708 | Connect: 74, Read: 36,245 |

#### Visualization

![Requests per Second](assets/requests_per_second.png)

![Average Latency](assets/average_latency.png)

#### Detailed Results

## Benchmark Results
**Areion**

#### Areion
```bash
Running 30s test @ http://localhost:8000/json
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 8.46ms 2.06ms 47.38ms 95.91%
Req/Sec 3.96k 430.06 5.36k 87.17%
1,418,550 requests in 30.03s, 132.58MB read
Socket errors: connect 0, read 545, write 0, timeout 0
Requests/sec: 47,241.97
Transfer/sec: 4.42MB
```

**FastAPI**

```bash
Running 30s test @ http://localhost:8000/json
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 7.15ms 1.34ms 38.99ms 97.58%
Req/Sec 4.66k 336.44 8.89k 92.00%
1,668,675 requests in 30.03s, 157.53MB read
Socket errors: connect 0, read 2,622, write 0, timeout 0
Non-2xx or 3xx responses: 1,980
Requests/sec: 55,566.74
Transfer/sec: 5.25MB
Latency 111.53ms 31.97ms 498.08ms 89.55%
Req/Sec 300.08 59.85 430.00 86.17%
107,613 requests in 30.07s, 15.60MB read
Socket errors: connect 0, read 419, write 0, timeout 0
Requests/sec: 3,579.10
Transfer/sec: 531.27KB
```

#### FastAPI
**Flask**

```bash
Running 30s test @ http://localhost:8000/json
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 73.42ms 10.92ms 190.27ms 89.45%
Req/Sec 450.92 71.28 653.00 73.33%
161,989 requests in 30.10s, 23.48MB read
Socket errors: connect 0, read 418, write 0, timeout 0
Requests/sec: 5,381.16
Transfer/sec: 798.78KB
Latency 47.45ms 53.33ms 556.78ms 96.24%
Req/Sec 183.12 104.16 590.00 70.64%
16,708 requests in 30.05s, 3.08MB read
Socket errors: connect 74, read 36,245, write 125, timeout 0
Requests/sec: 555.98
Transfer/sec: 104.79KB
```

### Analysis

#### Throughput (Requests per Second)

Areion: 55,566.74 requests/sec
FastAPI: 5,381.16 requests/sec
Areion handled approximately 10 times more requests per second than FastAPI.
- **Areion:** 47,241.97 requests/sec
- **FastAPI:** 3,579.10 requests/sec
- **Flask:** 555.98 requests/sec

Areion handled approximately 13 times more requests per second than FastAPI and 85 times more than Flask.

#### Average Latency

Areion: 7.15 ms
FastAPI: 73.42 ms
Areion's average latency is about 10 times lower than FastAPI's, indicating faster response times.
- **Areion:** 8.46 ms
- **Flask:** 47.45 ms
- **FastAPI:** 111.53 ms

Areion's average latency is about 5.6 times lower than Flask and 13 times lower than FastAPI, indicating faster response times.

#### Total Requests Handled

Areion: 1,668,675 requests
FastAPI: 161,989 requests
- **Areion:** 1,418,550 requests
- **FastAPI:** 107,613 requests
- **Flask:** 16,708 requests

Areion processed significantly more total requests during the test duration.

#### Socket Errors

Areion: 2,622 read errors
FastAPI: 418 read errors
Areion encountered more socket read errors due to the higher number of connections and requests.

## Table of Contents

- [Getting Started](#getting-started)
- [Installation](#installation)
- [Quick Start Guide](#quick-start-guide)
- [Enabling Development Tools](#enabling-development-tools)
- [Core Components](#core-components)
- [AreionServer](#areionserver)
- [Router](#router)
- [HttpServer](#httpserver)
- [Default Component Implementation](#default-component-implementation)
- [Orchestrator](#orchestrator)
- [Logger](#logger)
- [Engine](#engine)
- [Advanced Usage](#advanced-usage)
- [Middleware](#middleware)
- [Grouping Routes](#grouping-routes)
- [Template Rendering](#template-rendering)
- [Task Scheduling](#task-scheduling)
- [API Reference](#api-reference)
- [AreionServer API](#areionserver-api)
- [Router API](#router-api)
- [HttpRequest and HttpResponse](#httprequest-and-httpresponse)
- [Exception Handling](#exception-handling)
- [Contributing](#contributing)
- [License](#license)
- **Areion:** Read errors: 545
- **FastAPI:** Read errors: 419
- **Flask:** Connect errors: 74, Read errors: 36,245, Write errors: 125

---
For handling 10x the requests of FastAPI and 85x the requests of Flask, Areion had a relatively low number of socket errors.

## Getting Started

Expand Down
4 changes: 2 additions & 2 deletions areion/core/response.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import json
import orjson

HTTP_STATUS_CODES: dict[int, str] = {
100: "Continue",
Expand Down Expand Up @@ -111,7 +111,7 @@ def _format_body(self):
str or bytes: The formatted body.
"""
if isinstance(self.body, dict):
return json.dumps(self.body).encode("utf-8") # Convert dict to JSON
return orjson.dumps(self.body)
elif isinstance(self.body, str):
return self.body.encode("utf-8") # Convert string to bytes
elif isinstance(self.body, bytes):
Expand Down
62 changes: 41 additions & 21 deletions areion/core/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ def __init__(
self,
router,
request_factory,
logger=None,
host: str = "localhost",
port: int = 8080,
max_conns: int = 1000,
buffer_size: int = 8192,
keep_alive_timeout: int = 15,
keep_alive_timeout: int = 30,
):
if not isinstance(port, int):
raise ValueError("Port must be an integer.")
Expand All @@ -38,7 +39,7 @@ def __init__(
self.buffer_size = buffer_size
self.keep_alive_timeout = keep_alive_timeout
self._shutdown_event = asyncio.Event()
self.logger = None
self.logger = logger

async def _handle_client(self, reader, writer):
try:
Expand All @@ -54,36 +55,52 @@ async def _handle_client(self, reader, writer):
await writer.wait_closed()

async def _process_request(self, reader, writer):
# Ensures that the request is processed within the timeout
try:
await asyncio.wait_for(
self._handle_request_logic(reader, writer),
timeout=self.keep_alive_timeout,
)
except asyncio.TimeoutError:
response = HttpResponse(status_code=408, body="Request Timeout")
await self._send_response(writer, response)
await self._handle_request_logic(reader, writer)
except Exception as e:
if isinstance(e, ConnectionResetError):
self.log("debug", f"Connection reset by peer: {e}")
else:
self.log("error", f"Error processing request: {e}")
response = HttpResponse(status_code=500, body="Internal Server Error")
await self._send_response(writer, response)

async def _handle_request_logic(self, reader, writer):
while True:
try:
request_line = await asyncio.wait_for(
reader.readline(), timeout=self.keep_alive_timeout
data = await asyncio.wait_for(
reader.readuntil(b'\r\n\r\n'), timeout=self.keep_alive_timeout
)
except asyncio.TimeoutError:
break # No new request received within the keep-alive timeout
break
except asyncio.IncompleteReadError:
break
except asyncio.LimitOverrunError:
response = HttpResponse(status_code=413, body="Payload Too Large")
await self._send_response(writer, response)
break

if not request_line:
break # Client closed the connection
if not data:
break

method, path, _ = request_line.decode("utf-8").strip().split(" ")
headers = await self._parse_headers(reader)
try:
headers_end = data.find(b'\r\n\r\n')
header_data = data[:headers_end].decode('utf-8')
lines = header_data.split('\r\n')
request_line = lines[0]
header_lines = lines[1:]

request = self.request_factory.create(method, path, headers)
method, path, _ = request_line.strip().split(" ")
headers = {}
for line in header_lines:
if ': ' in line:
header_name, header_value = line.strip().split(": ", 1)
headers[header_name] = header_value

handler, path_params, is_async = self.router.get_handler(method, path)
request = self.request_factory.create(method, path, headers)

handler, path_params, is_async = self.router.get_handler(method, path)

try:
if not handler:
raise NotFoundError()

Expand All @@ -93,10 +110,11 @@ async def _handle_request_logic(self, reader, writer):
response = handler(request, **path_params)
except HttpError as e:
# Handles web exceptions raised by the handler
response = HttpResponse(status_code=e.status_code, body=e)
response = HttpResponse(status_code=e.status_code, body=str(e))
except Exception as e:
# Handles all other exceptions
response = HttpResponse(status_code=500, body="Internal Server Error")
self.log("error", f"Exception in request handling: {e}")

await self._send_response(writer, response)

Expand All @@ -120,6 +138,8 @@ async def _send_response(self, writer, response):
if not isinstance(response, HttpResponse):
response = HttpResponse(body=response)

# TODO: Add interceptor component here

buffer = response.format_response()
writer.write(buffer)
await writer.drain()
Expand Down
2 changes: 2 additions & 0 deletions areion/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,14 @@ async def start(self) -> None:

self._start_orchestrator_in_thread()

# TODO: Allow setting of max_conns, buffer_size, and keep_alive_timeout in builder and pass through here
# Add the HTTP Server
self.http_server = HttpServer(
router=self.router,
host=self.host,
port=self.port,
request_factory=self.request_factory,
logger=self.logger,
)

# Start the HTTP server
Expand Down
3 changes: 2 additions & 1 deletion areion/tests/core/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def test_json_response(self):

self.assertIn(b"HTTP/1.1 200 OK", formatted_response)
self.assertIn(b"Content-Type: application/json", formatted_response)
self.assertIn(b'"key": "value"', formatted_response)
self.assertIn(b'{"key":"value"}', formatted_response)


def test_html_response(self):
body = "<html><body>Hello, World!</body></html>"
Expand Down
Binary file added assets/average_latency.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/requests_per_second.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ classifiers = [
]
dependencies = [
"jinja2>=3.0.0",
"apscheduler>=3.0.0"
"apscheduler>=3.0.0",
"orjson>=3.10.7"
]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
apscheduler
jinja2
setuptools
asyncio
orjson >= 3.10.7

0 comments on commit 3dade6c

Please sign in to comment.