committed by
GitHub
3 changed files with 312 additions and 12 deletions
@ -0,0 +1,105 @@ |
|||||
|
import time |
||||
|
|
||||
|
from fastapi import APIRouter, FastAPI |
||||
|
|
||||
|
# Setup Router |
||||
|
app = FastAPI() |
||||
|
router = APIRouter() |
||||
|
|
||||
|
# 50 Static Routes |
||||
|
for i in range(50): |
||||
|
|
||||
|
@router.get(f"/static-{i}") |
||||
|
def static_route(i=i): |
||||
|
return {"route": f"static-{i}"} |
||||
|
|
||||
|
|
||||
|
# 50 Dynamic Routes |
||||
|
for i in range(50): |
||||
|
|
||||
|
@router.get(f"/dynamic-{i}/{{item_id}}") |
||||
|
def dynamic_route(item_id: int, i=i): |
||||
|
return {"route": f"dynamic-{i}", "item_id": item_id} |
||||
|
|
||||
|
|
||||
|
app.include_router(router) |
||||
|
from contextlib import AsyncExitStack |
||||
|
|
||||
|
import anyio |
||||
|
|
||||
|
|
||||
|
# Mock ASGI stack |
||||
|
async def mock_receive(): |
||||
|
return {"type": "http.request", "body": b"", "more_body": False} |
||||
|
|
||||
|
|
||||
|
async def mock_send(message): |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
scope_static = { |
||||
|
"type": "http", |
||||
|
"method": "GET", |
||||
|
"path": "/static-49", |
||||
|
"headers": [], |
||||
|
"query_string": b"", |
||||
|
"client": ("127.0.0.1", 1234), |
||||
|
"server": ("127.0.0.1", 80), |
||||
|
"fastapi_middleware_astack": AsyncExitStack(), |
||||
|
"fastapi_inner_astack": AsyncExitStack(), |
||||
|
"fastapi_function_astack": AsyncExitStack(), |
||||
|
} |
||||
|
|
||||
|
scope_dynamic = { |
||||
|
"type": "http", |
||||
|
"method": "GET", |
||||
|
"path": "/dynamic-49/12345", |
||||
|
"headers": [], |
||||
|
"query_string": b"", |
||||
|
"client": ("127.0.0.1", 1234), |
||||
|
"server": ("127.0.0.1", 80), |
||||
|
"fastapi_middleware_astack": AsyncExitStack(), |
||||
|
"fastapi_inner_astack": AsyncExitStack(), |
||||
|
"fastapi_function_astack": AsyncExitStack(), |
||||
|
} |
||||
|
|
||||
|
|
||||
|
async def run_benchmark(): |
||||
|
# Warmup |
||||
|
for _ in range(100): |
||||
|
await app.router.app(dict(scope_static), mock_receive, mock_send) |
||||
|
await app.router.app(dict(scope_dynamic), mock_receive, mock_send) |
||||
|
|
||||
|
print("Running Static Route Benchmark (10,000 iterations)...") |
||||
|
|
||||
|
# 1. Caching Enabled (standard flow) |
||||
|
# Warmup cache |
||||
|
await app.router.app(dict(scope_static), mock_receive, mock_send) |
||||
|
start_time = time.perf_counter() |
||||
|
for _ in range(10000): |
||||
|
await app.router.app(dict(scope_static), mock_receive, mock_send) |
||||
|
cache_enabled_time = time.perf_counter() - start_time |
||||
|
|
||||
|
# 2. Caching Disabled (clear cache before each request) |
||||
|
start_time = time.perf_counter() |
||||
|
for _ in range(10000): |
||||
|
app.router._route_cache.clear() |
||||
|
await app.router.app(dict(scope_static), mock_receive, mock_send) |
||||
|
cache_disabled_time = time.perf_counter() - start_time |
||||
|
|
||||
|
print(f"Static Route - Cache Enabled: {cache_enabled_time:.4f} seconds") |
||||
|
print(f"Static Route - Cache Disabled: {cache_disabled_time:.4f} seconds") |
||||
|
print(f"Speedup: {cache_disabled_time / cache_enabled_time:.2f}x") |
||||
|
|
||||
|
print("\nRunning Dynamic Route Benchmark (10,000 iterations)...") |
||||
|
|
||||
|
# Caching has no effect on dynamic routes |
||||
|
start_time = time.perf_counter() |
||||
|
for _ in range(10000): |
||||
|
await app.router.app(dict(scope_dynamic), mock_receive, mock_send) |
||||
|
dynamic_time = time.perf_counter() - start_time |
||||
|
print(f"Dynamic Route time: {dynamic_time:.4f} seconds") |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
anyio.run(run_benchmark) |
||||
@ -0,0 +1,74 @@ |
|||||
|
from fastapi import APIRouter, FastAPI |
||||
|
from fastapi.testclient import TestClient |
||||
|
|
||||
|
|
||||
|
def test_static_route_caching(): |
||||
|
app = FastAPI() |
||||
|
router = APIRouter() |
||||
|
|
||||
|
@router.get("/static") |
||||
|
def read_static(): |
||||
|
return {"status": "ok"} |
||||
|
|
||||
|
@router.get("/users/{user_id}") |
||||
|
def read_user(user_id: int): |
||||
|
return {"user_id": user_id} |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
# First request: Cache miss, resolves normally |
||||
|
response = client.get("/static") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"status": "ok"} |
||||
|
|
||||
|
# Check cache size (should be 1) |
||||
|
cache = app.router._route_cache |
||||
|
assert len(cache) == 1 |
||||
|
cache_key = ("http", "GET", "/static") |
||||
|
assert cache_key in cache |
||||
|
|
||||
|
# Second request: Cache hit, resolves via cache |
||||
|
response = client.get("/static") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"status": "ok"} |
||||
|
assert len(cache) == 1 |
||||
|
|
||||
|
# Dynamic request: matches but should not be cached |
||||
|
response = client.get("/users/123") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"user_id": 123} |
||||
|
assert cache_key in cache |
||||
|
# Verify dynamic path is not added to the cache |
||||
|
assert len(cache) == 1 |
||||
|
assert ("http", "GET", "/users/123") not in cache |
||||
|
|
||||
|
|
||||
|
def test_cache_invalidation(): |
||||
|
app = FastAPI() |
||||
|
router = APIRouter() |
||||
|
|
||||
|
@router.get("/health") |
||||
|
def health(): |
||||
|
return {"status": "healthy"} |
||||
|
|
||||
|
app.include_router(router) |
||||
|
client = TestClient(app) |
||||
|
|
||||
|
# First request |
||||
|
response = client.get("/health") |
||||
|
assert response.status_code == 200 |
||||
|
assert len(app.router._route_cache) == 1 |
||||
|
|
||||
|
# Invalidate by adding a new route dynamically |
||||
|
@app.get("/new-route") |
||||
|
def new_route(): |
||||
|
return {"new": "yes"} |
||||
|
|
||||
|
# Cache should be cleared |
||||
|
assert len(app.router._route_cache) == 0 |
||||
|
|
||||
|
# Request new route and health route to populate cache again |
||||
|
client.get("/new-route") |
||||
|
client.get("/health") |
||||
|
assert len(app.router._route_cache) == 2 |
||||
Loading…
Reference in new issue