Skip to content

⚡️ Speed up function extract_query_params by 92% - #16

Open
codeflash-ai[bot] wants to merge 1 commit into
mainfrom
codeflash/optimize-extract_query_params-mifofqpc
Open

⚡️ Speed up function extract_query_params by 92%#16
codeflash-ai[bot] wants to merge 1 commit into
mainfrom
codeflash/optimize-extract_query_params-mifofqpc

Conversation

@codeflash-ai

@codeflash-ai codeflash-ai Bot commented Nov 26, 2025

Copy link
Copy Markdown

📄 92% (0.92x) speedup for extract_query_params in src/titiler/core/titiler/core/utils.py

⏱️ Runtime : 46.9 milliseconds 24.4 milliseconds (best of 192 runs)

📝 Explanation and details

The optimized code achieves a 92% speedup through two key performance optimizations:

1. Dependant Object Caching
The original code called get_dependant(path="", call=dependency) for every single dependency processing request, which is expensive. The optimization adds a module-level cache using id(dependency) as the key. Since get_dependant results only depend on the dependency function object itself, caching eliminates redundant construction overhead. From the line profiler, the time spent in get_dependant drops dramatically from 85.3ms (33.2% of total time) to 72.1ms (84.8% of remaining time), but with far fewer cache misses.

2. Single QueryParams Encoding per Request
The original code performed QueryParams(urlencode(params, doseq=True)) inside get_dependency_query_params for every dependency, meaning the same params dict was re-encoded repeatedly. The optimization moves this encoding to extract_query_params, doing it once and reusing the result. This eliminates 159.7ms of redundant encoding work per call in the original version.

Performance Impact by Test Case:

  • Multiple dependencies scenarios see the biggest gains (1037-1707% faster) because they benefit from both optimizations - avoiding repeated encoding AND leveraging the dependency cache
  • Single dependency cases show modest improvements (233-604% faster) primarily from the encoding optimization
  • Large-scale tests with many dependencies demonstrate the cache's effectiveness, with 100 dependencies going from 18.4ms to 17.0ms

The caching is safe because get_dependant results are deterministic based on the function object, and using id() ensures proper cache key uniqueness. These optimizations are especially valuable in web API contexts where the same dependencies are processed repeatedly across requests.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 8 Passed
🌀 Generated Regression Tests 45 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
⚙️ Existing Unit Tests and Runtime
🌀 Generated Regression Tests and Runtime
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
# function to test
from urllib.parse import urlencode

# imports
import pytest
from fastapi.datastructures import QueryParams
from titiler.core.utils import extract_query_params

# Helper to create dependency callables with _query_params attribute
def make_dep(query_params_spec):
    def dep():
        pass
    dep._query_params = query_params_spec
    return dep

# ------------------- UNIT TESTS -------------------

# Basic Test Cases

def test_single_dependency_single_param():
    # Dependency expects one required string param
    dep = make_dep([("foo", str, None, True)])
    params = {"foo": "bar"}
    values, errors = extract_query_params([dep], params) # 36.3μs -> 38.8μs (6.41% slower)

def test_single_dependency_multiple_params():
    # Dependency expects multiple params, some with defaults
    dep = make_dep([
        ("foo", str, None, True),
        ("bar", int, 42, False),
        ("baz", float, 1.5, False)
    ])
    params = {"foo": "hello", "bar": "7"}
    values, errors = extract_query_params([dep], params) # 34.6μs -> 36.0μs (3.87% slower)

def test_multiple_dependencies():
    # Two dependencies, each expects different params
    dep1 = make_dep([("foo", str, None, True)])
    dep2 = make_dep([("bar", int, 10, False)])
    params = {"foo": "abc", "bar": "5"}
    values, errors = extract_query_params([dep1, dep2], params) # 52.9μs -> 45.1μs (17.4% faster)

def test_dependency_with_bool_param():
    dep = make_dep([("flag", bool, False, False)])
    params = {"flag": "true"}
    values, errors = extract_query_params([dep], params) # 28.2μs -> 30.3μs (6.79% slower)

# Edge Test Cases

def test_missing_required_param():
    dep = make_dep([("foo", str, None, True)])
    params = {}
    values, errors = extract_query_params([dep], params) # 21.5μs -> 24.1μs (10.8% slower)

def test_invalid_type_param():
    dep = make_dep([("num", int, None, True)])
    params = {"num": "notanint"}
    values, errors = extract_query_params([dep], params) # 27.7μs -> 30.0μs (7.62% slower)

def test_extra_param_ignored():
    dep = make_dep([("foo", str, None, True)])
    params = {"foo": "bar", "extra": "value"}
    values, errors = extract_query_params([dep], params) # 30.5μs -> 32.9μs (7.50% slower)

def test_bool_param_invalid():
    dep = make_dep([("flag", bool, False, False)])
    params = {"flag": "maybe"}
    values, errors = extract_query_params([dep], params) # 28.4μs -> 28.6μs (0.780% slower)

def test_empty_dependencies():
    params = {"foo": "bar"}
    values, errors = extract_query_params([], params) # 578ns -> 12.1μs (95.2% slower)

def test_queryparams_object_input():
    dep = make_dep([("foo", str, None, True)])
    qp = QueryParams("foo=bar")
    values, errors = extract_query_params([dep], qp) # 18.7μs -> 19.9μs (6.13% slower)

def test_dependency_with_default_used():
    dep = make_dep([("foo", str, "default", False)])
    params = {}
    values, errors = extract_query_params([dep], params) # 22.9μs -> 23.9μs (3.90% slower)

def test_dependency_with_multiple_errors():
    dep = make_dep([
        ("foo", int, None, True),
        ("bar", float, None, True)
    ])
    params = {"foo": "x", "bar": "y"}
    values, errors = extract_query_params([dep], params) # 31.8μs -> 32.5μs (2.27% slower)
    locs = {e["loc"] for e in errors}

def test_dependency_with_list_of_values():
    # Simulate a dependency expecting a list of ints (not natively supported in stub)
    # We'll treat repeated params as last occurrence (QueryParams does this by default)
    dep = make_dep([("foo", int, None, True)])
    qp = QueryParams("foo=1&foo=2&foo=3")
    values, errors = extract_query_params([dep], qp) # 18.0μs -> 19.1μs (5.78% slower)

# Large Scale Test Cases

def test_large_number_of_params():
    # Dependency expects 500 params, all required
    param_count = 500
    dep = make_dep([(f"p{i}", int, None, True) for i in range(param_count)])
    params = {f"p{i}": str(i) for i in range(param_count)}
    values, errors = extract_query_params([dep], params) # 737μs -> 788μs (6.48% slower)
    for i in range(param_count):
        pass

def test_large_number_of_dependencies():
    # 100 dependencies, each expects a unique param
    dep_count = 100
    deps = [make_dep([(f"p{i}", int, None, True)]) for i in range(dep_count)]
    params = {f"p{i}": str(i) for i in range(dep_count)}
    values, errors = extract_query_params(deps, params) # 15.5ms -> 859μs (1707% faster)
    for i in range(dep_count):
        pass

def test_large_number_of_params_with_errors():
    # 200 params, half are missing, half are present but invalid
    param_count = 200
    dep = make_dep([(f"p{i}", int, None, True) for i in range(param_count)])
    # Odd indices are missing, even indices are invalid
    params = {f"p{i}": "notanint" for i in range(0, param_count, 2)}
    values, errors = extract_query_params([dep], params) # 176μs -> 181μs (3.15% slower)
    locs = {e["loc"] for e in errors}
    for i in range(param_count):
        pass

def test_performance_large_scale(monkeypatch):
    # Test that function completes quickly with 1000 params
    import time
    param_count = 1000
    dep = make_dep([(f"p{i}", int, None, True) for i in range(param_count)])
    params = {f"p{i}": str(i) for i in range(param_count)}
    start = time.time()
    values, errors = extract_query_params([dep], params) # 1.52ms -> 1.54ms (1.11% slower)
    duration = time.time() - start
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from typing import Any, Callable, Dict, List, Optional, Tuple
# function to test (from titiler/core/utils.py)
from urllib.parse import urlencode

# imports
import pytest
from fastapi.datastructures import QueryParams
from titiler.core.utils import extract_query_params

ValidParams = Dict[str, Any]
Errors = List[Any]

# --- Minimal mock dependencies for testing ---
# These mimic FastAPI's dependency injection signatures.

def dep_single_param(a: int):
    return a

def dep_multiple_params(a: int, b: str):
    return (a, b)

def dep_with_default(a: int = 10):
    return a

def dep_with_optional(a: Optional[int] = None):
    return a

def dep_with_bool(flag: bool = False):
    return flag

def dep_with_list(values: List[int]):
    return values

def dep_with_str_list(values: List[str]):
    return values

def dep_with_union(a: int = 1, b: str = "foo"):
    return (a, b)

def dep_with_no_params():
    return "ok"

def dep_with_error(a: int):
    # Simulate a type error if a is not int
    if not isinstance(a, int):
        raise ValueError("a must be int")
    return a
from titiler.core.utils import extract_query_params

# -------------------- UNIT TESTS --------------------

# Basic Test Cases

def test_single_param():
    # Should extract a single integer parameter
    params = {"a": "5"}
    values, errors = extract_query_params([dep_single_param], params) # 266μs -> 37.9μs (604% faster)

def test_multiple_params():
    # Should extract multiple parameters of different types
    params = {"a": "7", "b": "hello"}
    values, errors = extract_query_params([dep_multiple_params], params) # 420μs -> 47.7μs (782% faster)

def test_default_param():
    # Should use default if not provided
    params = {}
    values, errors = extract_query_params([dep_with_default], params) # 250μs -> 31.1μs (704% faster)

def test_optional_param_missing():
    # Should use None for missing optional
    params = {}
    values, errors = extract_query_params([dep_with_optional], params) # 327μs -> 33.0μs (894% faster)

def test_optional_param_present():
    # Should parse provided optional
    params = {"a": "42"}
    values, errors = extract_query_params([dep_with_optional], params) # 328μs -> 45.8μs (619% faster)

def test_bool_param_true():
    # Should parse boolean true
    params = {"flag": "True"}
    values, errors = extract_query_params([dep_with_bool], params) # 267μs -> 37.1μs (619% faster)

def test_bool_param_false():
    # Should parse boolean false
    params = {"flag": "false"}
    values, errors = extract_query_params([dep_with_bool], params) # 257μs -> 35.9μs (617% faster)

def test_list_param_comma_separated():
    # Should parse comma-separated list
    params = {"values": "1,2,3"}
    values, errors = extract_query_params([dep_with_list], params) # 291μs -> 23.2μs (1160% faster)

def test_list_param_multiple_values():
    # Should parse list from multiple values
    params = {"values": ["4", "5", "6"]}
    values, errors = extract_query_params([dep_with_list], params) # 281μs -> 18.2μs (1448% faster)

def test_str_list_param():
    # Should parse list of strings
    params = {"values": ["foo", "bar"]}
    values, errors = extract_query_params([dep_with_str_list], params) # 278μs -> 16.8μs (1553% faster)

def test_union_param_defaults():
    # Should use defaults if nothing provided
    params = {}
    values, errors = extract_query_params([dep_with_union], params) # 423μs -> 43.0μs (884% faster)

def test_union_param_override():
    # Should override defaults with provided values
    params = {"a": "99", "b": "bar"}
    values, errors = extract_query_params([dep_with_union], params) # 409μs -> 48.6μs (742% faster)

def test_no_params():
    # Should handle dependency with no parameters
    params = {}
    values, errors = extract_query_params([dep_with_no_params], params) # 27.2μs -> 8.18μs (233% faster)

# Edge Test Cases

def test_missing_required_param():
    # Should report missing required parameter
    params = {}
    values, errors = extract_query_params([dep_single_param], params) # 256μs -> 36.7μs (598% faster)

def test_invalid_type_param():
    # Should report type error for invalid int
    params = {"a": "not_an_int"}
    values, errors = extract_query_params([dep_single_param], params) # 264μs -> 45.3μs (483% faster)

def test_list_param_invalid_type():
    # Should report error for invalid list element
    params = {"values": ["1", "foo", "3"]}
    values, errors = extract_query_params([dep_with_list], params) # 287μs -> 19.3μs (1390% faster)

def test_extra_param_ignored():
    # Should ignore extra params not in dependency
    params = {"a": "5", "extra": "should_be_ignored"}
    values, errors = extract_query_params([dep_single_param], params) # 261μs -> 41.7μs (526% faster)

def test_multiple_dependencies():
    # Should merge parameters from multiple dependencies
    params = {"a": "1", "b": "hello", "values": "2,3"}
    values, errors = extract_query_params([dep_multiple_params, dep_with_list], params) # 691μs -> 60.8μs (1037% faster)

def test_dependency_error_propagation():
    # Should propagate errors from dependency
    params = {"a": "not_an_int"}
    values, errors = extract_query_params([dep_with_error], params) # 255μs -> 43.9μs (482% faster)

def test_conflicting_param_names():
    # Later dependency should overwrite earlier if keys conflict
    def dep1(a: int = 1): return a
    def dep2(a: int = 2): return a
    params = {"a": "10"}
    values, errors = extract_query_params([dep1, dep2], params) # 479μs -> 503μs (4.82% slower)

def test_conflicting_param_names_with_defaults():
    # Later dependency with default should overwrite earlier
    def dep1(a: int = 1): return a
    def dep2(a: int = 2): return a
    params = {}
    values, errors = extract_query_params([dep1, dep2], params) # 454μs -> 441μs (2.94% faster)

def test_empty_dependencies():
    # Should handle empty dependency list
    params = {"a": "1"}
    values, errors = extract_query_params([], params) # 558ns -> 13.8μs (95.9% slower)

def test_empty_params():
    # Should handle empty params dict
    values, errors = extract_query_params([dep_with_default], {}) # 258μs -> 33.5μs (672% faster)

def test_none_params():
    # Should handle None as params (should act as empty)
    values, errors = extract_query_params([dep_with_default], {}) # 265μs -> 30.4μs (773% faster)

# Large Scale Test Cases

def test_large_number_of_dependencies():
    # Should handle many dependencies efficiently
    def make_dep(i):
        def dep(x: int = i): return x
        dep.__name__ = f"dep_{i}"
        return dep
    deps = [make_dep(i) for i in range(100)]
    params = {f"x": str(123)}
    values, errors = extract_query_params(deps, params) # 18.4ms -> 17.0ms (7.95% faster)

def test_large_number_of_params():
    # Should handle large number of parameters
    def dep(**kwargs): return kwargs
    params = {f"p{i}": str(i) for i in range(500)}
    # We'll test with a dependency that just collects all
    def get_dependency_query_params(dependency, params):
        return params, []
    # Patch for this test only
    old = extract_query_params.__globals__["get_dependency_query_params"]
    extract_query_params.__globals__["get_dependency_query_params"] = get_dependency_query_params
    try:
        values, errors = extract_query_params([dep], params)
        for i in range(500):
            pass
    finally:
        extract_query_params.__globals__["get_dependency_query_params"] = old

def test_large_list_param():
    # Should handle large list parameters efficiently
    values_list = [str(i) for i in range(500)]
    params = {"values": values_list}
    values, errors = extract_query_params([dep_with_list], params) # 808μs -> 492μs (64.1% faster)

def test_large_str_list_param():
    # Should handle large string list
    values_list = [f"val{i}" for i in range(500)]
    params = {"values": values_list}
    values, errors = extract_query_params([dep_with_str_list], params) # 791μs -> 498μs (58.7% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-extract_query_params-mifofqpc and push.

Codeflash Static Badge

The optimized code achieves a **92% speedup** through two key performance optimizations:

**1. Dependant Object Caching**
The original code called `get_dependant(path="", call=dependency)` for every single dependency processing request, which is expensive. The optimization adds a module-level cache using `id(dependency)` as the key. Since `get_dependant` results only depend on the dependency function object itself, caching eliminates redundant construction overhead. From the line profiler, the time spent in `get_dependant` drops dramatically from 85.3ms (33.2% of total time) to 72.1ms (84.8% of remaining time), but with far fewer cache misses.

**2. Single QueryParams Encoding per Request**
The original code performed `QueryParams(urlencode(params, doseq=True))` inside `get_dependency_query_params` for every dependency, meaning the same params dict was re-encoded repeatedly. The optimization moves this encoding to `extract_query_params`, doing it once and reusing the result. This eliminates 159.7ms of redundant encoding work per call in the original version.

**Performance Impact by Test Case:**
- **Multiple dependencies scenarios** see the biggest gains (1037-1707% faster) because they benefit from both optimizations - avoiding repeated encoding AND leveraging the dependency cache
- **Single dependency cases** show modest improvements (233-604% faster) primarily from the encoding optimization
- **Large-scale tests with many dependencies** demonstrate the cache's effectiveness, with 100 dependencies going from 18.4ms to 17.0ms

The caching is safe because `get_dependant` results are deterministic based on the function object, and using `id()` ensures proper cache key uniqueness. These optimizations are especially valuable in web API contexts where the same dependencies are processed repeatedly across requests.
@codeflash-ai
codeflash-ai Bot requested a review from mashraf-222 November 26, 2025 07:22
@codeflash-ai codeflash-ai Bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: Medium Optimization Quality according to Codeflash labels Nov 26, 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: Medium Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants