Skip to content

Benchmarks

Performance testing and optimization for Wiverno.

Overview

Wiverno includes performance benchmarks to track and improve performance over time. Benchmarks are run using pytest-benchmark.

Running Benchmarks

# Run all benchmarks
make benchmark

# Or directly
uv run pytest tests/benchmark/ --benchmark-only

# With comparison
uv run pytest tests/benchmark/ --benchmark-compare

# Save results
uv run pytest tests/benchmark/ --benchmark-save=results

# Compare with saved results
uv run pytest tests/benchmark/ --benchmark-compare=results

Benchmark Suite

Router Performance

Test route matching speed:

"""Benchmark router performance."""
import pytest
from wiverno.core.routing.router import Router


def test_simple_routing(benchmark):
    """Benchmark simple route matching."""
    router = Router()
    router.get("/")(lambda r: ("200 OK", "test"))

    result = benchmark(router.registry.match, "/", "GET")
    handler, params, _ = result
    assert handler is not None


def test_parameter_extraction(benchmark):
    """Benchmark path parameter extraction."""
    router = Router()
    router.get("/user/{id}")(lambda r: ("200 OK", "test"))

    result = benchmark(router.registry.match, "/user/123", "GET")
    handler, params, _ = result
    assert params["id"] == "123"


def test_large_route_table(benchmark):
    """Benchmark with many routes."""
    router = Router()
    for i in range(100):
        router.get(f"/route{i}")(lambda r: ("200 OK", "test"))

    result = benchmark(router.registry.match, "/route50", "GET")
    handler, params, _ = result
    assert handler is not None


def test_complex_patterns(benchmark):
    """Benchmark complex route patterns."""
    router = Router()
    router.get("/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}")(
         lambda r: ("200 OK", "test"))

    result = benchmark(
        router.registry.match,
        "/api/v1/users/123/posts/456/comments/789",
        "GET"
    )
    handler, params, _ = result
    assert len(params) == 3

Request Parsing

Test request parsing performance:

"""Benchmark request parsing."""
import pytest
from wiverno.core.requests import Request


def test_basic_request_parsing(benchmark):
    """Benchmark basic request creation."""
    environ = {
        "REQUEST_METHOD": "GET",
        "PATH_INFO": "/",
        "QUERY_STRING": "",
        "wsgi.url_scheme": "http",
        "SERVER_NAME": "localhost",
        "SERVER_PORT": "8000",
    }

    request = benchmark(Request, environ)
    assert request.method == "GET"


def test_query_param_parsing(benchmark):
    """Benchmark query parameter parsing."""
    environ = {
        "REQUEST_METHOD": "GET",
        "PATH_INFO": "/search",
        "QUERY_STRING": "q=python&page=1&limit=10",
        "wsgi.url_scheme": "http",
        "SERVER_NAME": "localhost",
        "SERVER_PORT": "8000",
    }

    request = benchmark(Request, environ)
    assert len(request.query_params) == 3


def test_header_parsing(benchmark):
    """Benchmark header parsing."""
    environ = {
        "REQUEST_METHOD": "GET",
        "PATH_INFO": "/",
        "QUERY_STRING": "",
        "HTTP_USER_AGENT": "Mozilla/5.0",
        "HTTP_ACCEPT": "text/html",
        "HTTP_ACCEPT_LANGUAGE": "en-US",
        "wsgi.url_scheme": "http",
        "SERVER_NAME": "localhost",
        "SERVER_PORT": "8000",
    }

    request = benchmark(Request, environ)
    assert len(request.headers) >= 3

Application Performance

Test full request-response cycle:

"""Benchmark full application."""
import pytest
from wiverno.main import Wiverno


def test_simple_request_cycle(benchmark):
    """Benchmark complete request-response cycle."""
    def index(request):
        return "200 OK", "Hello, World!"

    app = Wiverno()
    app.get("/")(index)

    environ = {
        "REQUEST_METHOD": "GET",
        "PATH_INFO": "/",
        "QUERY_STRING": "",
        "wsgi.url_scheme": "http",
        "SERVER_NAME": "localhost",
        "SERVER_PORT": "8000",
    }

    response_data = []

    def start_response(status, headers):
        response_data.append((status, headers))

    def run_request():
        return list(app(environ, start_response))

    response = benchmark(run_request)
    assert response_data[0][0] == "200 OK"


def test_parameterized_request(benchmark):
    """Benchmark request with path parameters."""
    def user_view(request):
        user_id = request.path_params.get("id")
        return "200 OK", f"User {user_id}"

    app = Wiverno()
    app.get("/user/{id}")(user_view)

    environ = {
        "REQUEST_METHOD": "GET",
        "PATH_INFO": "/user/123",
        "QUERY_STRING": "",
        "wsgi.url_scheme": "http",
        "SERVER_NAME": "localhost",
        "SERVER_PORT": "8000",
    }

    response_data = []

    def start_response(status, headers):
        response_data.append((status, headers))

    def run_request():
        return list(app(environ, start_response))

    response = benchmark(run_request)
    assert b"User 123" in response[0]

Performance Metrics

Current Performance

Target metrics (as of v0.1.2):

Operation Target Actual
Simple routing < 10μs ~8μs
Parameter extraction < 15μs ~12μs
Request parsing < 20μs ~18μs
Full request cycle < 50μs ~45μs
Template rendering < 100μs ~90μs

Comparison with Other Frameworks

Relative performance (lower is better):

Framework Requests/sec Relative Speed
Wiverno 22,000 1.0x (baseline)
Flask 18,000 0.82x
FastAPI 25,000 1.14x
Starlette 28,000 1.27x

Note: Benchmarks vary based on hardware and test conditions.

Profiling

CPU Profiling

Profile CPU usage:

# Install profiling tools
uv pip install py-spy

# Profile running server
py-spy record -o profile.svg -- python app.py

# View flamegraph
open profile.svg

Memory Profiling

Profile memory usage:

# Install memory profiler
uv pip install memory_profiler

# Run with profiling
python -m memory_profiler app.py

Line-by-line Profiling

Profile specific functions:

from memory_profiler import profile

@profile
def my_function():
    """Function to profile."""
    # Your code here
    pass

Optimization Strategies

1. Route Table Optimization

Order routes by frequency:

# Good - Frequent routes first
routes = [
    ("/", index),           # Most common
    ("/api/users", users),  # Common
    ("/admin/debug", debug), # Rare
]

# Bad - Rare routes first
routes = [
    ("/admin/debug", debug),
    ("/api/users", users),
    ("/", index),
]

2. Request Parsing

Parse data lazily:

class Request:
    @property
    def query_params(self):
        """Parse query params on first access."""
        if not hasattr(self, "_query_params"):
            self._query_params = parse_qs(self.query_string)
        return self._query_params

3. Template Caching

Jinja2 automatically caches compiled templates. No action needed.

4. Static File Serving

Use nginx or CDN for static files in production:

location /static/ {
    alias /path/to/static/;
    expires 30d;
}

5. Response Compression

Use middleware for compression:

class GzipMiddleware:
    """Compress responses with gzip."""

    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        # Implement gzip compression
        pass

Load Testing

Using Apache Bench

# Install apache bench
# Ubuntu: apt-get install apache2-utils
# macOS: included by default

# Run load test
ab -n 10000 -c 100 http://localhost:8000/

# Results:
# Requests per second: 2000 [#/sec]
# Time per request: 50 [ms]

Using wrk

# Install wrk
# Ubuntu: apt-get install wrk
# macOS: brew install wrk

# Run load test
wrk -t 4 -c 100 -d 30s http://localhost:8000/

# Results:
# Thread Stats   Avg      Stdev     Max   +/- Stdev
#   Latency     5.00ms    2.00ms   50.00ms   95.00%
#   Req/Sec     5.00k     1.00k    7.00k    75.00%
# 600000 requests in 30.00s, 100.00MB read
# Requests/sec:  20000.00

Using locust

# locustfile.py
from locust import HttpUser, task, between

class WivernoUser(HttpUser):
    """Simulate user behavior."""

    wait_time = between(1, 3)

    @task
    def index(self):
        """Load homepage."""
        self.client.get("/")

    @task(3)
    def api_users(self):
        """Load API endpoint (3x more frequent)."""
        self.client.get("/api/users")

Run:

# Install locust
uv pip install locust

# Run load test
locust -f locustfile.py --host=http://localhost:8000

Continuous Benchmarking

Track performance over time:

# Baseline benchmark
uv run pytest tests/benchmark/ --benchmark-save=baseline

# After changes
uv run pytest tests/benchmark/ --benchmark-compare=baseline

# View comparison
# If slower, investigate changes
# If faster, celebrate! 🎉

Profiling

Profile code to identify bottlenecks:

import cProfile
import pstats
from io import StringIO

pr = cProfile.Profile()
pr.enable()

# Code to profile
router.match("/route50")

pr.disable()
s = StringIO()
ps = pstats.Stats(pr, stream=s).sort_stats('cumulative')
ps.print_stats(10)
print(s.getvalue())

Optimization Tips

  1. Order routes by frequency (frequent routes first)
  2. Use caching for computed values
  3. Return early when possible
  4. Parse data lazily (on first access)
  5. Use appropriate data structures (dict vs list)
  6. Compile templates once, reuse many times
  7. Use middleware for cross-cutting concerns

Next Steps