Skip to content

⚡️ Speed up function extract_query_params by 3,851% - #21

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

⚡️ Speed up function extract_query_params by 3,851%#21
codeflash-ai[bot] wants to merge 1 commit into
mainfrom
codeflash/optimize-extract_query_params-miha4eur

Conversation

@codeflash-ai

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

Copy link
Copy Markdown

📄 3,851% (38.51x) speedup for extract_query_params in src/titiler/core/titiler/core/utils.py

⏱️ Runtime : 392 milliseconds 9.92 milliseconds (best of 250 runs)

📝 Explanation and details

The optimization achieves a 3850% speedup by eliminating redundant query parameter processing in loops. Here's what changed:

Key Optimization:

  • Hoisted expensive encoding out of loops: The original code called QueryParams(urlencode(params, doseq=True)) inside get_dependency_query_params for every dependency, even when processing the same params multiple times. The optimized version introduces _to_query_params() helper and moves this conversion to happen once in extract_query_params before the loop.

Why This is Dramatically Faster:

  • urlencode() is expensive - it serializes dictionary data to URL-encoded strings
  • QueryParams() constructor then parses that string back into a structured format
  • In scenarios with many dependencies (like the test with 500 dependencies), this encoding/parsing happened 500 times for identical input
  • The optimization reduces this from O(n) expensive operations to O(1)

Performance Impact by Test Case:

  • Small workloads (1-3 dependencies): Modest 4-30% improvements due to reduced function call overhead
  • Large workloads (100+ dependencies): Massive improvements - up to 9334% faster for 500 dependencies
  • Edge cases with no dependencies show slight regression due to added helper function call, but this is negligible in real usage

Fast Path Benefits:
The isinstance(params, QueryParams) check provides a fast path when params is already in the correct format, avoiding unnecessary conversion entirely.

This optimization is particularly valuable for applications processing multiple query parameter dependencies simultaneously, which is common in web API frameworks like FastAPI where this code operates.

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, 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

ValidParams = Dict[str, Any]
Errors = List[Any]
from titiler.core.utils import extract_query_params

# Helper to create test dependency callables with expected query params
def make_dep(*param_names):
    def dep():
        pass
    dep._query_params = param_names
    return dep

# unit tests

# Basic Test Cases

def test_single_param_present():
    # Test extracting a single param that is present
    dep = make_dep("foo")
    params = {"foo": "bar"}
    values, errors = extract_query_params([dep], params) # 36.3μs -> 33.7μs (7.70% faster)

def test_single_param_missing():
    # Test extracting a single param that is missing
    dep = make_dep("foo")
    params = {}
    values, errors = extract_query_params([dep], params) # 24.9μs -> 23.8μs (4.66% faster)

def test_multiple_params_all_present():
    # Test extracting multiple params, all present
    dep = make_dep("a", "b", "c")
    params = {"a": "1", "b": "2", "c": "3"}
    values, errors = extract_query_params([dep], params) # 34.9μs -> 33.5μs (4.35% faster)

def test_multiple_params_some_missing():
    # Test extracting multiple params, some missing
    dep = make_dep("x", "y", "z")
    params = {"x": "alpha"}
    values, errors = extract_query_params([dep], params) # 30.0μs -> 28.4μs (5.55% faster)

def test_multiple_dependencies():
    # Test extracting params from multiple dependencies
    dep1 = make_dep("foo")
    dep2 = make_dep("bar")
    params = {"foo": "123", "bar": "456"}
    values, errors = extract_query_params([dep1, dep2], params) # 51.3μs -> 40.5μs (26.7% faster)

def test_multiple_dependencies_with_overlap():
    # Test extracting params from dependencies with overlapping param names
    dep1 = make_dep("foo", "bar")
    dep2 = make_dep("bar", "baz")
    params = {"foo": "1", "bar": "2", "baz": "3"}
    values, errors = extract_query_params([dep1, dep2], params) # 54.3μs -> 41.8μs (29.8% faster)

def test_input_as_queryparams():
    # Test passing QueryParams instead of dict
    dep = make_dep("foo", "bar")
    qp = QueryParams("foo=hello&bar=world")
    values, errors = extract_query_params([dep], qp) # 17.2μs -> 16.3μs (5.80% faster)

# Edge Test Cases

def test_no_dependencies():
    # Test with empty dependencies list
    params = {"foo": "bar"}
    values, errors = extract_query_params([], params) # 520ns -> 11.8μs (95.6% slower)

def test_no_params():
    # Test with empty params and non-empty dependencies
    dep = make_dep("foo")
    values, errors = extract_query_params([dep], {}) # 23.3μs -> 22.2μs (4.85% faster)

def test_empty_params_and_dependencies():
    # Both dependencies and params are empty
    values, errors = extract_query_params([], {}) # 592ns -> 5.88μs (89.9% slower)

def test_param_with_empty_string_value():
    # Param is present but value is empty string
    dep = make_dep("foo")
    params = {"foo": ""}
    values, errors = extract_query_params([dep], params) # 31.2μs -> 29.4μs (6.22% faster)

def test_param_with_none_value():
    # Param is present with None value (should be treated as missing)
    dep = make_dep("foo")
    params = {"foo": None}
    values, errors = extract_query_params([dep], params) # 31.3μs -> 29.8μs (5.00% faster)

def test_param_with_list_value():
    # Param is present as a list (simulate repeated query param)
    dep = make_dep("foo")
    params = {"foo": ["a", "b", "c"]}
    values, errors = extract_query_params([dep], params) # 33.0μs -> 32.1μs (2.84% faster)

def test_dependency_with_no_params():
    # Dependency that expects no params
    dep = make_dep()
    params = {"foo": "bar"}
    values, errors = extract_query_params([dep], params) # 28.8μs -> 28.0μs (3.02% faster)

def test_param_with_special_characters():
    # Param value with special characters
    dep = make_dep("q")
    params = {"q": "hello world!@#$%^&*()"}
    values, errors = extract_query_params([dep], params) # 45.2μs -> 41.4μs (9.23% faster)

def test_param_with_unicode():
    # Param value with unicode characters
    dep = make_dep("emoji")
    params = {"emoji": "😀🐍"}
    values, errors = extract_query_params([dep], params) # 42.8μs -> 39.1μs (9.59% faster)

def test_param_with_integer_value():
    # Param value is an integer
    dep = make_dep("num")
    params = {"num": 42}
    values, errors = extract_query_params([dep], params) # 30.4μs -> 28.4μs (7.29% faster)

def test_case_sensitivity():
    # Param names are case sensitive
    dep = make_dep("foo")
    params = {"FOO": "bar"}
    values, errors = extract_query_params([dep], params) # 28.4μs -> 27.0μs (5.17% faster)

# Large Scale Test Cases

def test_large_number_of_params():
    # Test with a large number of params (1000)
    keys = [f"key{i}" for i in range(1000)]
    dep = make_dep(*keys)
    params = {k: str(i) for i, k in enumerate(keys)}
    values, errors = extract_query_params([dep], params) # 1.57ms -> 1.54ms (1.78% faster)

def test_large_number_of_dependencies():
    # Test with a large number of dependencies, each expecting a unique param
    deps = [make_dep(f"p{i}") for i in range(500)]
    params = {f"p{i}": f"v{i}" for i in range(500)}
    values, errors = extract_query_params(deps, params) # 363ms -> 3.85ms (9334% faster)

def test_large_number_of_missing_params():
    # Test with a large number of expected params, none provided
    keys = [f"missing{i}" for i in range(500)]
    dep = make_dep(*keys)
    params = {}
    values, errors = extract_query_params([dep], params) # 29.8μs -> 23.2μs (28.3% faster)

def test_large_mixed_present_and_missing():
    # Test with half present, half missing
    keys = [f"key{i}" for i in range(200)]
    dep = make_dep(*keys)
    params = {k: f"val{i}" for i, k in enumerate(keys[:100])}
    values, errors = extract_query_params([dep], params) # 187μs -> 179μs (4.24% faster)

def test_large_multiple_dependencies_overlap():
    # Test with many dependencies, overlapping params
    deps = [make_dep(f"p{i}", f"p{i+1}") for i in range(0, 100, 2)]
    params = {f"p{i}": f"v{i}" for i in range(101)}
    values, errors = extract_query_params(deps, params) # 8.04ms -> 494μs (1526% faster)
    # All keys from p0 to p100 should be present
    expected = {f"p{i}": f"v{i}" for i in range(101)}
# 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, 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

ValidParams = Dict[str, Any]
Errors = List[Any]
from titiler.core.utils import extract_query_params

# --- Dependency Definitions for Testing ---

def dep_int_required():
    pass
dep_int_required._query_params = [("a", int, None, True)]

def dep_str_optional():
    pass
dep_str_optional._query_params = [("b", str, "default", False)]

def dep_multiple_types():
    pass
dep_multiple_types._query_params = [
    ("x", int, None, True),
    ("y", float, 1.5, False),
    ("z", str, "zval", False),
]

def dep_no_params():
    pass
dep_no_params._query_params = []

def dep_bool():
    pass
dep_bool._query_params = [("flag", lambda v: v.lower() in ("true", "1"), False, False)]

# --- Unit Tests ---

# 1. Basic Test Cases

def test_single_required_param_present():
    # Should extract the required int param correctly
    params = {"a": "123"}
    values, errors = extract_query_params([dep_int_required], params) # 29.2μs -> 27.7μs (5.47% faster)

def test_single_required_param_missing():
    # Should return error for missing required param
    params = {}
    values, errors = extract_query_params([dep_int_required], params) # 22.1μs -> 20.7μs (7.12% faster)

def test_optional_param_with_default():
    # Should use default if not present
    params = {}
    values, errors = extract_query_params([dep_str_optional], params) # 23.0μs -> 20.2μs (13.8% faster)

def test_optional_param_provided():
    # Should use provided value, not default
    params = {"b": "hello"}
    values, errors = extract_query_params([dep_str_optional], params) # 29.3μs -> 27.7μs (5.75% faster)

def test_multiple_params_mixed():
    # Should parse required and optional, with and without defaults
    params = {"x": "5", "z": "abc"}
    values, errors = extract_query_params([dep_multiple_types], params) # 31.7μs -> 29.7μs (6.71% faster)

def test_multiple_params_all_provided():
    params = {"x": "7", "y": "2.5", "z": "foo"}
    values, errors = extract_query_params([dep_multiple_types], params) # 34.2μs -> 32.3μs (5.86% faster)

def test_multiple_dependencies():
    # Should merge values from both dependencies
    params = {"a": "42", "b": "test"}
    values, errors = extract_query_params([dep_int_required, dep_str_optional], params) # 54.4μs -> 39.3μs (38.4% faster)

def test_no_params_dependency():
    # Should handle dependency with no query params
    params = {"foo": "bar"}
    values, errors = extract_query_params([dep_no_params], params) # 28.1μs -> 26.3μs (6.54% faster)

def test_bool_param_true():
    params = {"flag": "TrUe"}
    values, errors = extract_query_params([dep_bool], params) # 28.5μs -> 26.1μs (9.04% faster)

def test_bool_param_false():
    params = {"flag": "0"}
    values, errors = extract_query_params([dep_bool], params) # 27.3μs -> 26.8μs (1.84% faster)

# 2. Edge Test Cases

def test_wrong_type_param():
    # Should catch conversion error
    params = {"a": "not_an_int"}
    values, errors = extract_query_params([dep_int_required], params) # 28.2μs -> 26.8μs (5.41% faster)

def test_multiple_errors():
    # Both required int and required x missing
    params = {}
    values, errors = extract_query_params([dep_int_required, dep_multiple_types], params) # 36.6μs -> 30.3μs (20.6% faster)

def test_extra_params_ignored():
    # Extra params not in dependency should be ignored
    params = {"a": "1", "unknown": "value"}
    values, errors = extract_query_params([dep_int_required], params) # 31.7μs -> 30.1μs (5.38% faster)

def test_queryparams_input():
    # Should accept QueryParams as input
    qp = QueryParams("a=99")
    values, errors = extract_query_params([dep_int_required], qp) # 17.4μs -> 15.7μs (10.8% faster)

def test_empty_dependencies():
    # Should handle empty dependencies list
    params = {"a": "1"}
    values, errors = extract_query_params([], params) # 574ns -> 11.0μs (94.8% slower)

def test_dependency_with_all_defaults():
    # All params optional, none provided
    def dep_all_defaults():
        pass
    dep_all_defaults._query_params = [("d1", int, 10, False), ("d2", str, "hi", False)]
    params = {}
    values, errors = extract_query_params([dep_all_defaults], params) # 22.5μs -> 22.2μs (1.53% faster)

def test_dependency_with_empty_params():
    # Dependency expects param, but empty string provided
    params = {"a": ""}
    values, errors = extract_query_params([dep_int_required], params) # 30.1μs -> 27.1μs (10.9% faster)

# 3. Large Scale Test Cases

def make_large_dep(n):
    # Returns a dependency with n required int params named p0, p1, ...
    def dep():
        pass
    dep._query_params = [(f"p{i}", int, None, True) for i in range(n)]
    return dep

def test_large_number_of_params_all_provided():
    n = 200
    dep = make_large_dep(n)
    params = {f"p{i}": str(i) for i in range(n)}
    values, errors = extract_query_params([dep], params) # 328μs -> 321μs (2.16% faster)

def test_large_number_of_params_some_missing():
    n = 200
    dep = make_large_dep(n)
    params = {f"p{i}": str(i) for i in range(0, n, 2)}  # only even params
    values, errors = extract_query_params([dep], params) # 179μs -> 177μs (1.26% faster)
    for i in range(1, n, 2):
        pass
    # Even params parsed
    for i in range(0, n, 2):
        pass

def test_large_number_of_dependencies():
    # 100 dependencies, each expects a unique param
    deps = []
    params = {}
    for i in range(100):
        def make_dep(idx):
            def dep():
                pass
            dep._query_params = [(f"k{idx}", int, None, True)]
            return dep
        dep = make_dep(i)
        deps.append(dep)
        params[f"k{i}"] = str(i*2)
    values, errors = extract_query_params(deps, params) # 15.9ms -> 802μs (1882% faster)

def test_large_number_of_optional_params():
    # 500 optional params, none provided
    def dep():
        pass
    dep._query_params = [(f"opt{i}", int, i, False) for i in range(500)]
    params = {}
    values, errors = extract_query_params([dep], params) # 23.0μs -> 21.1μs (8.93% faster)

def test_large_mixed_types_and_values():
    # 100 required ints, 100 optional floats, 100 optional strs
    def dep():
        pass
    dep._query_params = (
        [(f"i{i}", int, None, True) for i in range(100)] +
        [(f"f{i}", float, 1.0+i, False) for i in range(100)] +
        [(f"s{i}", str, f"str{i}", False) for i in range(100)]
    )
    params = {f"i{i}": str(i*3) for i in range(100)}
    params.update({f"f{i}": str(i*0.5) for i in range(0, 100, 2)})  # half provided
    values, errors = extract_query_params([dep], params) # 253μs -> 249μs (1.54% faster)
    # All required ints present
    for i in range(100):
        pass
    # Optional floats: provided or default
    for i in range(100):
        if i % 2 == 0:
            pass
        else:
            pass
    # Optional strs: all default
    for i in range(100):
        pass
# 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-miha4eur and push.

Codeflash Static Badge

The optimization achieves a **3850% speedup** by eliminating redundant query parameter processing in loops. Here's what changed:

**Key Optimization:**
- **Hoisted expensive encoding out of loops**: The original code called `QueryParams(urlencode(params, doseq=True))` inside `get_dependency_query_params` for every dependency, even when processing the same `params` multiple times. The optimized version introduces `_to_query_params()` helper and moves this conversion to happen once in `extract_query_params` before the loop.

**Why This is Dramatically Faster:**
- `urlencode()` is expensive - it serializes dictionary data to URL-encoded strings
- `QueryParams()` constructor then parses that string back into a structured format
- In scenarios with many dependencies (like the test with 500 dependencies), this encoding/parsing happened 500 times for identical input
- The optimization reduces this from O(n) expensive operations to O(1)

**Performance Impact by Test Case:**
- **Small workloads** (1-3 dependencies): Modest 4-30% improvements due to reduced function call overhead
- **Large workloads** (100+ dependencies): Massive improvements - up to 9334% faster for 500 dependencies
- **Edge cases** with no dependencies show slight regression due to added helper function call, but this is negligible in real usage

**Fast Path Benefits:**
The `isinstance(params, QueryParams)` check provides a fast path when `params` is already in the correct format, avoiding unnecessary conversion entirely.

This optimization is particularly valuable for applications processing multiple query parameter dependencies simultaneously, which is common in web API frameworks like FastAPI where this code operates.
@codeflash-ai
codeflash-ai Bot requested a review from mashraf-222 November 27, 2025 10:17
@codeflash-ai codeflash-ai Bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 27, 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.

0 participants