Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Oct 29, 2025

📄 66% (0.66x) speedup for Host.matches in starlette/routing.py

⏱️ Runtime : 2.23 milliseconds 1.35 milliseconds (best of 5 runs)

📝 Explanation and details

The optimized version achieves a 65% speedup by eliminating expensive object allocations and method lookups in the hot path:

Key optimizations:

  1. Direct header parsing: Instead of creating a Headers object on every request, the code directly iterates through the raw scope["headers"] list to find the host header. This avoids object instantiation and the overhead of the Headers class's generic lookup mechanism.

  2. Cached bound method: The self.host_regex.match method is cached as self._host_regex_match during initialization, eliminating repeated attribute lookups in the hot loop.

  3. Optimized parameter conversion:

    • Uses a local reference convertors = self.param_convertors to avoid repeated attribute access
    • Iterates over keys only instead of items(), reducing dictionary overhead
    • Uses direct key access instead of unpacking tuples
  4. Efficient path_params handling: Uses dict unpacking ({**path_params, **matched_params}) instead of dict() constructor + update() calls, and conditionally creates the merged dict only when existing path_params exist.

Performance characteristics based on tests:

  • Simple cases (no params): ~66% faster - benefits most from header parsing optimization
  • Parameter-heavy cases: ~38-45% faster - benefits from both header parsing and parameter conversion optimizations
  • Edge cases (missing headers): Slightly slower due to fallback, but these are rare in practice
  • Large-scale tests: ~68% faster - compound benefits of all optimizations

The optimization is most effective for typical web traffic patterns where host headers are consistently present and parameter counts are moderate.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 1036 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 97.6%
🌀 Generated Regression Tests and Runtime
import re

# imports
import pytest  # used for our unit tests
from starlette.routing import Host

# --- Minimal stubs for dependencies ---

# Minimal CONVERTOR_TYPES and Convertor to support the tests
class Convertor:
    def __init__(self, regex, convert):
        self.regex = regex
        self.convert = convert

# Only basic types for testing
CONVERTOR_TYPES = {
    "str": Convertor(regex="[^/]+", convert=str),
    "int": Convertor(regex="[0-9]+", convert=int),
    "float": Convertor(regex="[0-9]+(?:\\.[0-9]+)?", convert=float),
}

# Minimal Match enum
class Match:
    FULL = "full"
    NONE = "none"

# Minimal Headers implementation for test
class Headers:
    def __init__(self, scope):
        self._scope = scope
    def get(self, key, default=None):
        for k, v in self._scope.get("headers", []):
            if k.decode().lower() == key.lower():
                return v.decode()
        return default

# Minimal BaseRoute for inheritance
class BaseRoute:
    pass

# --- matches function and Host class ---

PARAM_REGEX = re.compile("{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")

def compile_path(path: str):
    is_host = not path.startswith("/")
    path_regex = "^"
    path_format = ""
    duplicated_params = set()
    idx = 0
    param_convertors = {}
    for match in PARAM_REGEX.finditer(path):
        param_name, convertor_type = match.groups("str")
        convertor_type = convertor_type.lstrip(":")
        convertor = CONVERTOR_TYPES[convertor_type]
        path_regex += re.escape(path[idx : match.start()])
        path_regex += f"(?P<{param_name}>{convertor.regex})"
        path_format += path[idx : match.start()]
        path_format += "{%s}" % param_name
        if param_name in param_convertors:
            duplicated_params.add(param_name)
        param_convertors[param_name] = convertor
        idx = match.end()
    if duplicated_params:
        names = ", ".join(sorted(duplicated_params))
        ending = "s" if len(duplicated_params) > 1 else ""
        raise ValueError(f"Duplicated param name{ending} {names} at path {path}")
    if is_host:
        hostname = path[idx:].split(":")[0]
        path_regex += re.escape(hostname) + "$"
    else:
        path_regex += re.escape(path[idx:]) + "$"
    path_format += path[idx:]
    return re.compile(path_regex), path_format, param_convertors
from starlette.routing import Host

# --- Unit Tests ---

# Helper function to create scope with host header
def make_scope(host, type_="http", path_params=None):
    headers = [(b"host", host.encode())]
    scope = {"type": type_, "headers": headers}
    if path_params:
        scope["path_params"] = path_params
    return scope

# Dummy ASGI app
dummy_app = object()

# --- 1. Basic Test Cases ---

def test_matches_simple_str_param():
    # Host pattern with one string param
    host = Host("{subdomain}.example.com", dummy_app)
    scope = make_scope("foo.example.com")
    match_type, child_scope = host.matches(scope) # 10.2μs -> 7.87μs (29.8% faster)

def test_matches_simple_int_param():
    host = Host("{id:int}.example.com", dummy_app)
    scope = make_scope("123.example.com")
    match_type, child_scope = host.matches(scope) # 8.88μs -> 6.18μs (43.8% faster)

def test_matches_multiple_params():
    host = Host("{subdomain}.{region}.example.com", dummy_app)
    scope = make_scope("foo.us.example.com")
    match_type, child_scope = host.matches(scope) # 8.89μs -> 6.31μs (40.9% faster)

def test_matches_no_params():
    host = Host("www.example.com", dummy_app)
    scope = make_scope("www.example.com")
    match_type, child_scope = host.matches(scope) # 6.87μs -> 4.15μs (65.6% faster)

def test_matches_with_existing_path_params_merges():
    host = Host("{subdomain}.example.com", dummy_app)
    scope = make_scope("foo.example.com", path_params={"existing": 42})
    match_type, child_scope = host.matches(scope) # 7.79μs -> 5.08μs (53.2% faster)

# --- 2. Edge Test Cases ---

def test_matches_unmatched_host_returns_none():
    host = Host("{subdomain}.example.com", dummy_app)
    scope = make_scope("bar.notexample.com")
    match_type, child_scope = host.matches(scope) # 5.67μs -> 3.43μs (65.2% faster)

def test_matches_wrong_type_param():
    host = Host("{id:int}.example.com", dummy_app)
    scope = make_scope("abc.example.com")  # 'abc' cannot convert to int
    match_type, child_scope = host.matches(scope) # 5.21μs -> 2.79μs (86.9% faster)

def test_matches_missing_host_header():
    host = Host("{subdomain}.example.com", dummy_app)
    scope = {"type": "http", "headers": []}
    match_type, child_scope = host.matches(scope) # 5.33μs -> 9.39μs (43.3% slower)

def test_matches_websocket_type():
    host = Host("{subdomain}.example.com", dummy_app)
    scope = make_scope("foo.example.com", type_="websocket")
    match_type, child_scope = host.matches(scope) # 8.25μs -> 5.23μs (57.8% faster)

def test_matches_non_http_ws_type():
    host = Host("{subdomain}.example.com", dummy_app)
    scope = make_scope("foo.example.com", type_="lifespan")
    match_type, child_scope = host.matches(scope) # 1.09μs -> 979ns (11.8% faster)

def test_matches_duplicate_param_raises():
    # Should raise ValueError for duplicate param name
    with pytest.raises(ValueError) as excinfo:
        Host("{foo}.{foo}.example.com", dummy_app)

def test_matches_unknown_convertor_type_raises():
    # Should raise AssertionError for unknown convertor
    with pytest.raises(AssertionError) as excinfo:
        Host("{foo:unknown}.example.com", dummy_app)

def test_matches_host_with_port_ignored():
    host = Host("{subdomain}.example.com", dummy_app)
    scope = make_scope("foo.example.com:8080")
    match_type, child_scope = host.matches(scope) # 9.12μs -> 5.64μs (61.5% faster)

def test_matches_float_param():
    host = Host("{num:float}.example.com", dummy_app)
    scope = make_scope("123.45.example.com")
    match_type, child_scope = host.matches(scope) # 9.59μs -> 6.60μs (45.3% faster)

def test_matches_param_with_underscore_and_digits():
    host = Host("{foo_bar123}.example.com", dummy_app)
    scope = make_scope("baz123.example.com")
    match_type, child_scope = host.matches(scope) # 8.12μs -> 4.80μs (69.4% faster)

def test_matches_host_must_not_start_with_slash():
    # Should raise assertion error
    with pytest.raises(AssertionError):
        Host("/foo.example.com", dummy_app)

# --- 3. Large Scale Test Cases ---


def test_matches_many_int_params():
    param_names = [f"p{i}" for i in range(50)]
    host_pattern = ".".join([f"{{{name}:int}}" for name in param_names]) + ".example.com"
    host = Host(host_pattern, dummy_app)
    host_value = ".".join([str(i) for i in range(50)]) + ".example.com"
    scope = make_scope(host_value)
    match_type, child_scope = host.matches(scope) # 30.3μs -> 22.0μs (37.5% faster)
    for i, name in enumerate(param_names):
        pass


def test_matches_performance_many_calls():
    # Test performance by matching 1000 hosts
    host = Host("{subdomain}.example.com", dummy_app)
    for i in range(1000):
        scope = make_scope(f"foo{i}.example.com")
        match_type, child_scope = host.matches(scope) # 2.09ms -> 1.25ms (67.7% faster)

def test_matches_large_param_value():
    # Large param value
    large_value = "x" * 500
    host = Host("{subdomain}.example.com", dummy_app)
    scope = make_scope(f"{large_value}.example.com")
    match_type, child_scope = host.matches(scope) # 9.23μs -> 5.82μs (58.5% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
#------------------------------------------------
import re

# imports
import pytest  # used for our unit tests
from starlette.routing import Host

# function to test
# We'll define a minimal version of matches for testing purposes.
# For this test suite, let's assume matches takes two arguments:
# - pattern: a string with parameters, e.g. "/user/{id:int}"
# - path: a string to match, e.g. "/user/42"
# It returns a dict of matched parameters if the path matches, or None otherwise.

# We'll implement a correct version for the tests.
_PARAM_REGEX = re.compile(r"{([a-zA-Z_][a-zA-Z0-9_]*)(:[a-zA-Z_][a-zA-Z0-9_]*)?}")

_CONVERTOR_TYPES = {
    "str": (r"[^/]+", str),
    "int": (r"\d+", int),
    "float": (r"\d+\.\d+", float),
    "path": (r".+", str),
}

def matches(pattern: str, path: str):
    """
    Match a path against a pattern with parameters.
    Returns a dict of matched parameters (with type conversion), or None.
    """
    idx = 0
    regex = "^"
    param_info = []
    for m in _PARAM_REGEX.finditer(pattern):
        name, typ = m.groups("str")
        typ = typ.lstrip(":")
        if typ not in _CONVERTOR_TYPES:
            raise ValueError(f"Unknown convertor type '{typ}'")
        regex += re.escape(pattern[idx:m.start()])
        regex += f"(?P<{name}>{_CONVERTOR_TYPES[typ][0]})"
        param_info.append((name, _CONVERTOR_TYPES[typ][1]))
        idx = m.end()
    regex += re.escape(pattern[idx:]) + "$"
    match = re.match(regex, path)
    if not match:
        return None
    result = {}
    for name, typ in param_info:
        result[name] = typ(match.group(name))
    return result

# unit tests

# 1. Basic Test Cases

To edit these changes git checkout codeflash/optimize-Host.matches-mhbpin64 and push.

Codeflash

The optimized version achieves a 65% speedup by eliminating expensive object allocations and method lookups in the hot path:

**Key optimizations:**

1. **Direct header parsing**: Instead of creating a `Headers` object on every request, the code directly iterates through the raw `scope["headers"]` list to find the host header. This avoids object instantiation and the overhead of the Headers class's generic lookup mechanism.

2. **Cached bound method**: The `self.host_regex.match` method is cached as `self._host_regex_match` during initialization, eliminating repeated attribute lookups in the hot loop.

3. **Optimized parameter conversion**: 
   - Uses a local reference `convertors = self.param_convertors` to avoid repeated attribute access
   - Iterates over keys only instead of items(), reducing dictionary overhead
   - Uses direct key access instead of unpacking tuples

4. **Efficient path_params handling**: Uses dict unpacking (`{**path_params, **matched_params}`) instead of `dict()` constructor + `update()` calls, and conditionally creates the merged dict only when existing path_params exist.

**Performance characteristics based on tests:**
- Simple cases (no params): ~66% faster - benefits most from header parsing optimization
- Parameter-heavy cases: ~38-45% faster - benefits from both header parsing and parameter conversion optimizations  
- Edge cases (missing headers): Slightly slower due to fallback, but these are rare in practice
- Large-scale tests: ~68% faster - compound benefits of all optimizations

The optimization is most effective for typical web traffic patterns where host headers are consistently present and parameter counts are moderate.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 October 29, 2025 08:01
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant