Skip to content

Commit e89d23b

Browse files
committed
fixtures: fix catastrophic performance problem in reorder_items
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 1eee63a commit e89d23b

File tree

3 files changed

+33
-7
lines changed

3 files changed

+33
-7
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

+13-7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from typing import MutableMapping
2626
from typing import NoReturn
2727
from typing import Optional
28+
from typing import OrderedDict
2829
from typing import overload
2930
from typing import Sequence
3031
from typing import Set
@@ -77,8 +78,6 @@
7778

7879

7980
if TYPE_CHECKING:
80-
from typing import Deque
81-
8281
from _pytest.main import Session
8382
from _pytest.python import CallSpec2
8483
from _pytest.python import Function
@@ -215,16 +214,18 @@ def get_parametrized_fixture_argkeys(
215214

216215
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
217216
argkeys_by_item: Dict[Scope, Dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {}
218-
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
217+
items_by_argkey: Dict[
218+
Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
219+
] = {}
219220
for scope in HIGH_SCOPES:
220221
scoped_argkeys_by_item = argkeys_by_item[scope] = {}
221-
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
222+
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
222223
for item in items:
223224
argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope))
224225
if argkeys:
225226
scoped_argkeys_by_item[item] = argkeys
226227
for argkey in argkeys:
227-
scoped_items_by_argkey[argkey].append(item)
228+
scoped_items_by_argkey[argkey][item] = None
228229

229230
items_set = dict.fromkeys(items)
230231
return list(
@@ -237,7 +238,9 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
237238
def reorder_items_atscope(
238239
items: OrderedSet[nodes.Item],
239240
argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]],
240-
items_by_argkey: Mapping[Scope, Mapping[FixtureArgKey, "Deque[nodes.Item]"]],
241+
items_by_argkey: Mapping[
242+
Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]]
243+
],
241244
scope: Scope,
242245
) -> OrderedSet[nodes.Item]:
243246
if scope is Scope.Function or len(items) < 3:
@@ -274,7 +277,10 @@ def reorder_items_atscope(
274277
for other_scope in HIGH_SCOPES:
275278
other_scoped_items_by_argkey = items_by_argkey[other_scope]
276279
for argkey in argkeys_by_item[other_scope].get(i, ()):
277-
other_scoped_items_by_argkey[argkey].appendleft(i)
280+
other_scoped_items_by_argkey[argkey][i] = None
281+
other_scoped_items_by_argkey[argkey].move_to_end(
282+
i, last=False
283+
)
278284
break
279285
if no_argkey_items:
280286
reordered_no_argkey_items = reorder_items_atscope(

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)