Skip to content

Commit 153a436

Browse files
committed
[8.2.x] fixtures: fix catastrophic performance problem in reorder_items
Manual minimal backport from commit e89d23b. 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.
1 parent b41d5a5 commit 153a436

File tree

3 files changed

+31
-8
lines changed

3 files changed

+31
-8
lines changed

Diff for: changelog/12355.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters.

Diff for: src/_pytest/fixtures.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from typing import MutableMapping
2424
from typing import NoReturn
2525
from typing import Optional
26+
from typing import OrderedDict
2627
from typing import overload
2728
from typing import Sequence
2829
from typing import Set
@@ -75,8 +76,6 @@
7576

7677

7778
if TYPE_CHECKING:
78-
from typing import Deque
79-
8079
from _pytest.main import Session
8180
from _pytest.python import CallSpec2
8281
from _pytest.python import Function
@@ -207,16 +206,18 @@ def get_parametrized_fixture_keys(
207206

208207
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
209208
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
210-
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
209+
items_by_argkey: Dict[
210+
Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
211+
] = {}
211212
for scope in HIGH_SCOPES:
212213
scoped_argkeys_cache = argkeys_cache[scope] = {}
213-
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
214+
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
214215
for item in items:
215216
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
216217
if keys:
217218
scoped_argkeys_cache[item] = keys
218219
for key in keys:
219-
scoped_items_by_argkey[key].append(item)
220+
scoped_items_by_argkey[key][item] = None
220221
items_dict = dict.fromkeys(items, None)
221222
return list(
222223
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]:
226227
def fix_cache_order(
227228
item: nodes.Item,
228229
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
229-
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
230+
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
230231
) -> None:
231232
for scope in HIGH_SCOPES:
233+
scoped_items_by_argkey = items_by_argkey[scope]
232234
for key in argkeys_cache[scope].get(item, []):
233-
items_by_argkey[scope][key].appendleft(item)
235+
scoped_items_by_argkey[key][item] = None
236+
scoped_items_by_argkey[key].move_to_end(item, last=False)
234237

235238

236239
def reorder_items_atscope(
237240
items: Dict[nodes.Item, None],
238241
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
239-
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
242+
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
240243
scope: Scope,
241244
) -> Dict[nodes.Item, None]:
242245
if scope is Scope.Function or len(items) < 3:

Diff for: testing/python/fixtures.py

+19
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,25 @@ def test_check():
22192219
reprec = pytester.inline_run("-s")
22202220
reprec.assertoutcome(passed=2)
22212221

2222+
def test_reordering_catastrophic_performance(self, pytester: Pytester) -> None:
2223+
"""Check that a certain high-scope parametrization pattern doesn't cause
2224+
a catasrophic slowdown.
2225+
2226+
Regression test for #12355.
2227+
"""
2228+
pytester.makepyfile("""
2229+
import pytest
2230+
2231+
params = tuple("abcdefghijklmnopqrstuvwxyz")
2232+
@pytest.mark.parametrize(params, [range(len(params))] * 3, scope="module")
2233+
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):
2234+
pass
2235+
""")
2236+
2237+
result = pytester.runpytest()
2238+
2239+
result.assert_outcomes(passed=3)
2240+
22222241

22232242
class TestFixtureMarker:
22242243
def test_parametrize(self, pytester: Pytester) -> None:

0 commit comments

Comments
 (0)