[8.2.x] fixtures: fix catastrophic performance problem in `reorder_items`
Manual minimal backport from commit e89d23b247
.
Fix #12355.
In the issue, it was reported that the `reorder_items` has quadratic (or
worse...) behavior with certain simple parametrizations. After some
debugging I found that the problem happens because the "Fix
items_by_argkey order" loop keeps adding the same item to the deque,
and it reaches epic sizes which causes the slowdown.
I don't claim to understand how the `reorder_items` algorithm works, but
if as far as I understand, if an item already exists in the deque, the
correct thing to do is to move it to the front. Since a deque doesn't
have such an (efficient) operation, this switches to `OrderedDict` which
can efficiently append from both sides, deduplicate and move to front.
This commit is contained in:
parent
b41d5a52bb
commit
153a436bc4
|
@ -0,0 +1 @@
|
||||||
|
Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters.
|
|
@ -23,6 +23,7 @@ from typing import List
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import OrderedDict
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
@ -75,8 +76,6 @@ if sys.version_info < (3, 11):
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Deque
|
|
||||||
|
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.python import CallSpec2
|
from _pytest.python import CallSpec2
|
||||||
from _pytest.python import Function
|
from _pytest.python import Function
|
||||||
|
@ -207,16 +206,18 @@ def get_parametrized_fixture_keys(
|
||||||
|
|
||||||
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
|
items_by_argkey: Dict[
|
||||||
|
Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
|
||||||
|
] = {}
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
scoped_argkeys_cache = argkeys_cache[scope] = {}
|
scoped_argkeys_cache = argkeys_cache[scope] = {}
|
||||||
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
|
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
|
||||||
for item in items:
|
for item in items:
|
||||||
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
|
||||||
if keys:
|
if keys:
|
||||||
scoped_argkeys_cache[item] = keys
|
scoped_argkeys_cache[item] = keys
|
||||||
for key in keys:
|
for key in keys:
|
||||||
scoped_items_by_argkey[key].append(item)
|
scoped_items_by_argkey[key][item] = None
|
||||||
items_dict = dict.fromkeys(items, None)
|
items_dict = dict.fromkeys(items, None)
|
||||||
return list(
|
return list(
|
||||||
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
|
||||||
|
@ -226,17 +227,19 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
|
||||||
def fix_cache_order(
|
def fix_cache_order(
|
||||||
item: nodes.Item,
|
item: nodes.Item,
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
|
||||||
) -> None:
|
) -> None:
|
||||||
for scope in HIGH_SCOPES:
|
for scope in HIGH_SCOPES:
|
||||||
|
scoped_items_by_argkey = items_by_argkey[scope]
|
||||||
for key in argkeys_cache[scope].get(item, []):
|
for key in argkeys_cache[scope].get(item, []):
|
||||||
items_by_argkey[scope][key].appendleft(item)
|
scoped_items_by_argkey[key][item] = None
|
||||||
|
scoped_items_by_argkey[key].move_to_end(item, last=False)
|
||||||
|
|
||||||
|
|
||||||
def reorder_items_atscope(
|
def reorder_items_atscope(
|
||||||
items: Dict[nodes.Item, None],
|
items: Dict[nodes.Item, None],
|
||||||
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
|
||||||
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
|
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
) -> Dict[nodes.Item, None]:
|
) -> Dict[nodes.Item, None]:
|
||||||
if scope is Scope.Function or len(items) < 3:
|
if scope is Scope.Function or len(items) < 3:
|
||||||
|
|
|
@ -2219,6 +2219,25 @@ class TestAutouseManagement:
|
||||||
reprec = pytester.inline_run("-s")
|
reprec = pytester.inline_run("-s")
|
||||||
reprec.assertoutcome(passed=2)
|
reprec.assertoutcome(passed=2)
|
||||||
|
|
||||||
|
def test_reordering_catastrophic_performance(self, pytester: Pytester) -> None:
|
||||||
|
"""Check that a certain high-scope parametrization pattern doesn't cause
|
||||||
|
a catasrophic slowdown.
|
||||||
|
|
||||||
|
Regression test for #12355.
|
||||||
|
"""
|
||||||
|
pytester.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
params = tuple("abcdefghijklmnopqrstuvwxyz")
|
||||||
|
@pytest.mark.parametrize(params, [range(len(params))] * 3, scope="module")
|
||||||
|
def test_parametrize(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z):
|
||||||
|
pass
|
||||||
|
""")
|
||||||
|
|
||||||
|
result = pytester.runpytest()
|
||||||
|
|
||||||
|
result.assert_outcomes(passed=3)
|
||||||
|
|
||||||
|
|
||||||
class TestFixtureMarker:
|
class TestFixtureMarker:
|
||||||
def test_parametrize(self, pytester: Pytester) -> None:
|
def test_parametrize(self, pytester: Pytester) -> None:
|
||||||
|
|
Loading…
Reference in New Issue