cython path_matcher added to reduce time on hot operations
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Benchmark script for path_matcher performance comparison.
|
||||
|
||||
Compares:
|
||||
- Pure Python implementation
|
||||
- Cython implementation (if available)
|
||||
- Original MountedApp from asgi_mount.py
|
||||
|
||||
Usage:
|
||||
python benchmarks/bench_path_matcher.py
|
||||
"""
|
||||
|
||||
import time
|
||||
import statistics
|
||||
from typing import Callable, List, Tuple
|
||||
|
||||
from pyserve._path_matcher_py import (
|
||||
FastMountedPath as PyFastMountedPath,
|
||||
FastMountManager as PyFastMountManager,
|
||||
path_matches_prefix as py_path_matches_prefix,
|
||||
)
|
||||
|
||||
try:
|
||||
from pyserve._path_matcher import (
|
||||
FastMountedPath as CyFastMountedPath,
|
||||
FastMountManager as CyFastMountManager,
|
||||
path_matches_prefix as cy_path_matches_prefix,
|
||||
)
|
||||
CYTHON_AVAILABLE = True
|
||||
except ImportError:
|
||||
CYTHON_AVAILABLE = False
|
||||
print("Cython module not compiled. Run: python setup_cython.py build_ext --inplace\n")
|
||||
|
||||
from pyserve.asgi_mount import MountedApp
|
||||
|
||||
|
||||
def benchmark(func: Callable, iterations: int = 100000) -> Tuple[float, float]:
|
||||
times = []
|
||||
|
||||
for _ in range(1000):
|
||||
func()
|
||||
|
||||
for _ in range(iterations):
|
||||
start = time.perf_counter_ns()
|
||||
func()
|
||||
end = time.perf_counter_ns()
|
||||
times.append(end - start)
|
||||
|
||||
return statistics.mean(times), statistics.stdev(times)
|
||||
|
||||
|
||||
def format_time(ns: float) -> str:
|
||||
if ns < 1000:
|
||||
return f"{ns:.1f} ns"
|
||||
elif ns < 1_000_000:
|
||||
return f"{ns/1000:.2f} µs"
|
||||
else:
|
||||
return f"{ns/1_000_000:.2f} ms"
|
||||
|
||||
|
||||
def run_benchmarks():
|
||||
print("=" * 70)
|
||||
print("PATH MATCHER BENCHMARK")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
# Test paths
|
||||
mount_path = "/api/v1"
|
||||
test_paths = [
|
||||
"/api/v1/users/123/posts", # Matching - long
|
||||
"/api/v1", # Matching - exact
|
||||
"/api/v2/users", # Not matching - similar prefix
|
||||
"/other/path", # Not matching - completely different
|
||||
]
|
||||
|
||||
iterations = 100000
|
||||
|
||||
# =========================================================================
|
||||
# Benchmark 1: Single path matching
|
||||
# =========================================================================
|
||||
print("BENCHMARK 1: Single Path Matching")
|
||||
print("-" * 70)
|
||||
print(f" Mount path: {mount_path}")
|
||||
print(f" Iterations: {iterations:,}")
|
||||
print()
|
||||
|
||||
results = {}
|
||||
|
||||
# Original MountedApp
|
||||
original_mount = MountedApp(mount_path, app=None, name="test") # type: ignore
|
||||
|
||||
for test_path in test_paths:
|
||||
print(f" Test path: {test_path}")
|
||||
|
||||
# Original
|
||||
mean, std = benchmark(lambda: original_mount.matches(test_path), iterations)
|
||||
results[("Original", test_path)] = mean
|
||||
print(f" Original MountedApp: {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
# Pure Python
|
||||
py_mount = PyFastMountedPath(mount_path)
|
||||
mean, std = benchmark(lambda: py_mount.matches(test_path), iterations)
|
||||
results[("Python", test_path)] = mean
|
||||
print(f" Pure Python: {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
# Cython (if available)
|
||||
if CYTHON_AVAILABLE:
|
||||
cy_mount = CyFastMountedPath(mount_path)
|
||||
mean, std = benchmark(lambda: cy_mount.matches(test_path), iterations)
|
||||
results[("Cython", test_path)] = mean
|
||||
print(f" Cython: {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
print()
|
||||
|
||||
# =========================================================================
|
||||
# Benchmark 2: Mount Manager lookup
|
||||
# =========================================================================
|
||||
print()
|
||||
print("BENCHMARK 2: Mount Manager Lookup (10 mounts)")
|
||||
print("-" * 70)
|
||||
|
||||
# Setup managers with 10 mounts
|
||||
mount_paths = [f"/api/v{i}" for i in range(10)]
|
||||
|
||||
py_manager = PyFastMountManager()
|
||||
for p in mount_paths:
|
||||
py_manager.add_mount(PyFastMountedPath(p, name=p))
|
||||
|
||||
if CYTHON_AVAILABLE:
|
||||
cy_manager = CyFastMountManager()
|
||||
for p in mount_paths:
|
||||
cy_manager.add_mount(CyFastMountedPath(p, name=p))
|
||||
|
||||
test_lookups = [
|
||||
"/api/v5/users/123", # Middle mount
|
||||
"/api/v0/items", # First mount (longest)
|
||||
"/api/v9/data", # Last mount
|
||||
"/other/not/found", # No match
|
||||
]
|
||||
|
||||
for test_path in test_lookups:
|
||||
print(f" Lookup path: {test_path}")
|
||||
|
||||
# Pure Python
|
||||
mean, std = benchmark(lambda: py_manager.get_mount(test_path), iterations)
|
||||
print(f" Pure Python: {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
# Cython
|
||||
if CYTHON_AVAILABLE:
|
||||
mean, std = benchmark(lambda: cy_manager.get_mount(test_path), iterations)
|
||||
print(f" Cython: {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
print()
|
||||
|
||||
# =========================================================================
|
||||
# Benchmark 3: Combined match + modify
|
||||
# =========================================================================
|
||||
print()
|
||||
print("BENCHMARK 3: Combined Match + Modify Path")
|
||||
print("-" * 70)
|
||||
|
||||
from pyserve._path_matcher_py import match_and_modify_path as py_match_modify
|
||||
if CYTHON_AVAILABLE:
|
||||
from pyserve._path_matcher import match_and_modify_path as cy_match_modify
|
||||
|
||||
test_path = "/api/v1/users/123/posts"
|
||||
print(f" Test path: {test_path}")
|
||||
print(f" Mount path: {mount_path}")
|
||||
print()
|
||||
|
||||
# Original (separate calls)
|
||||
def original_match_modify():
|
||||
if original_mount.matches(test_path):
|
||||
return original_mount.get_modified_path(test_path)
|
||||
return None
|
||||
|
||||
mean, std = benchmark(original_match_modify, iterations)
|
||||
print(f" Original (2 calls): {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
# Pure Python combined
|
||||
mean, std = benchmark(lambda: py_match_modify(test_path, mount_path), iterations)
|
||||
print(f" Pure Python (combined): {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
# Cython combined
|
||||
if CYTHON_AVAILABLE:
|
||||
mean, std = benchmark(lambda: cy_match_modify(test_path, mount_path), iterations)
|
||||
print(f" Cython (combined): {format_time(mean):>12} ± {format_time(std)}")
|
||||
|
||||
# =========================================================================
|
||||
# Summary
|
||||
# =========================================================================
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("SUMMARY")
|
||||
print("=" * 70)
|
||||
|
||||
if CYTHON_AVAILABLE:
|
||||
print("Cython module is available and was benchmarked")
|
||||
else:
|
||||
print("Cython module not available - only Pure Python was benchmarked")
|
||||
print(" To build Cython module:")
|
||||
print(" 1. Install Cython: pip install cython")
|
||||
print(" 2. Build: python setup_cython.py build_ext --inplace")
|
||||
|
||||
print()
|
||||
print("The optimized path matcher provides:")
|
||||
print(" - Pre-computed path length and trailing slash")
|
||||
print(" - Boundary-aware prefix matching (prevents /api matching /api-v2)")
|
||||
print(" - Combined match+modify operation to reduce function calls")
|
||||
print(" - Longest-prefix-first ordering in MountManager")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_benchmarks()
|
||||
Reference in New Issue
Block a user