python: fix quadratic behavior in collection of items using xunit fixtures
Since commit 0f918b1a9d
pytest uses auto-generated autouse
pytest fixtures for the xunit fixtures
{setup,teardown}_{module,class,method,function}. All of these fixtures
were given the same name.
Unfortunately, pytest fixture lookup for a name works by grabbing all of
the fixtures globally declared with a name and filtering to only those
which match the specific node. So each xunit-using item iterates over a
list (of fixturedefs) of a size of all previous same-xunit-using items,
i.e. quadratic.
Fixing this properly to use a better data structure is likely to take
some effort, but we can avoid the immediate problem by just using
a different name for each item's autouse fixture, so it only matches
itself.
A benchmark is added to demonstrate the issue. It is still way too slow
after the fix and possibly still quadratic, but for a different reason
which is another matter.
Running --collect-only, before (snipped):
202533232 function calls (201902604 primitive calls) in 86.379 seconds
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 85.688 85.688 main.py:320(pytest_collection)
1 0.000 0.000 85.688 85.688 main.py:567(perform_collect)
80557/556 0.021 0.000 85.050 0.153 {method 'extend' of 'list' objects}
85001/15001 0.166 0.000 85.045 0.006 main.py:785(genitems)
10002 0.050 0.000 84.219 0.008 runner.py:455(collect_one_node)
10002 0.049 0.000 83.763 0.008 runner.py:340(pytest_make_collect_report)
10002 0.079 0.000 83.668 0.008 runner.py:298(from_call)
10002 0.019 0.000 83.541 0.008 runner.py:341(<lambda>)
5001 0.184 0.000 81.922 0.016 python.py:412(collect)
5000 0.020 0.000 81.072 0.016 python.py:842(collect)
30003 0.118 0.000 78.478 0.003 python.py:218(pytest_pycollect_makeitem)
30000 0.190 0.000 77.957 0.003 python.py:450(_genfunctions)
40001 0.081 0.000 76.664 0.002 nodes.py:183(from_parent)
30000 0.087 0.000 76.629 0.003 python.py:1595(from_parent)
40002 0.092 0.000 76.583 0.002 nodes.py:102(_create)
30000 0.305 0.000 76.404 0.003 python.py:1533(__init__)
15000 0.132 0.000 74.765 0.005 fixtures.py:1439(getfixtureinfo)
15000 0.165 0.000 73.664 0.005 fixtures.py:1492(getfixtureclosure)
15000 0.044 0.000 57.584 0.004 fixtures.py:1653(getfixturedefs)
30000 18.840 0.001 57.540 0.002 fixtures.py:1668(_matchfactories)
37507500 31.352 0.000 38.700 0.000 nodes.py:76(ischildnode)
15000 10.464 0.001 15.806 0.001 fixtures.py:1479(_getautousenames)
112930587/112910019 7.333 0.000 7.339 0.000 {built-in method builtins.len}
After:
51890333 function calls (51259706 primitive calls) in 27.306 seconds
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 26.783 26.783 main.py:320(pytest_collection)
1 0.000 0.000 26.783 26.783 main.py:567(perform_collect)
80557/556 0.020 0.000 26.108 0.047 {method 'extend' of 'list' objects}
85001/15001 0.151 0.000 26.103 0.002 main.py:785(genitems)
10002 0.047 0.000 25.324 0.003 runner.py:455(collect_one_node)
10002 0.045 0.000 24.888 0.002 runner.py:340(pytest_make_collect_report)
10002 0.069 0.000 24.805 0.002 runner.py:298(from_call)
10002 0.017 0.000 24.690 0.002 runner.py:341(<lambda>)
5001 0.168 0.000 23.150 0.005 python.py:412(collect)
5000 0.019 0.000 22.223 0.004 python.py:858(collect)
30003 0.101 0.000 19.818 0.001 python.py:218(pytest_pycollect_makeitem)
30000 0.161 0.000 19.368 0.001 python.py:450(_genfunctions)
30000 0.302 0.000 18.236 0.001 python.py:1611(from_parent)
40001 0.084 0.000 18.051 0.000 nodes.py:183(from_parent)
40002 0.116 0.000 17.967 0.000 nodes.py:102(_create)
30000 0.308 0.000 17.770 0.001 python.py:1549(__init__)
15000 0.117 0.000 16.111 0.001 fixtures.py:1439(getfixtureinfo)
15000 0.134 0.000 15.135 0.001 fixtures.py:1492(getfixtureclosure)
15000 9.320 0.001 14.738 0.001 fixtures.py:1479(_getautousenames)
This commit is contained in:
parent
0d9e27a363
commit
50114d4731
|
@ -0,0 +1,11 @@
|
||||||
|
for i in range(5000):
|
||||||
|
exec(
|
||||||
|
f"""
|
||||||
|
class Test{i}:
|
||||||
|
@classmethod
|
||||||
|
def setup_class(cls): pass
|
||||||
|
def test_1(self): pass
|
||||||
|
def test_2(self): pass
|
||||||
|
def test_3(self): pass
|
||||||
|
"""
|
||||||
|
)
|
|
@ -522,7 +522,12 @@ class Module(nodes.File, PyCollector):
|
||||||
if setup_module is None and teardown_module is None:
|
if setup_module is None and teardown_module is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@fixtures.fixture(autouse=True, scope="module")
|
@fixtures.fixture(
|
||||||
|
autouse=True,
|
||||||
|
scope="module",
|
||||||
|
# Use a unique name to speed up lookup.
|
||||||
|
name=f"xunit_setup_module_fixture_{self.obj.__name__}",
|
||||||
|
)
|
||||||
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
|
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
|
||||||
if setup_module is not None:
|
if setup_module is not None:
|
||||||
_call_with_optional_argument(setup_module, request.module)
|
_call_with_optional_argument(setup_module, request.module)
|
||||||
|
@ -546,7 +551,12 @@ class Module(nodes.File, PyCollector):
|
||||||
if setup_function is None and teardown_function is None:
|
if setup_function is None and teardown_function is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@fixtures.fixture(autouse=True, scope="function")
|
@fixtures.fixture(
|
||||||
|
autouse=True,
|
||||||
|
scope="function",
|
||||||
|
# Use a unique name to speed up lookup.
|
||||||
|
name=f"xunit_setup_function_fixture_{self.obj.__name__}",
|
||||||
|
)
|
||||||
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
|
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
|
||||||
if request.instance is not None:
|
if request.instance is not None:
|
||||||
# in this case we are bound to an instance, so we need to let
|
# in this case we are bound to an instance, so we need to let
|
||||||
|
@ -789,7 +799,12 @@ class Class(PyCollector):
|
||||||
if setup_class is None and teardown_class is None:
|
if setup_class is None and teardown_class is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@fixtures.fixture(autouse=True, scope="class")
|
@fixtures.fixture(
|
||||||
|
autouse=True,
|
||||||
|
scope="class",
|
||||||
|
# Use a unique name to speed up lookup.
|
||||||
|
name=f"xunit_setup_class_fixture_{self.obj.__qualname__}",
|
||||||
|
)
|
||||||
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
|
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
|
||||||
if setup_class is not None:
|
if setup_class is not None:
|
||||||
func = getimfunc(setup_class)
|
func = getimfunc(setup_class)
|
||||||
|
@ -813,7 +828,12 @@ class Class(PyCollector):
|
||||||
if setup_method is None and teardown_method is None:
|
if setup_method is None and teardown_method is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@fixtures.fixture(autouse=True, scope="function")
|
@fixtures.fixture(
|
||||||
|
autouse=True,
|
||||||
|
scope="function",
|
||||||
|
# Use a unique name to speed up lookup.
|
||||||
|
name=f"xunit_setup_method_fixture_{self.obj.__qualname__}",
|
||||||
|
)
|
||||||
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
|
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
|
||||||
method = request.function
|
method = request.function
|
||||||
if setup_method is not None:
|
if setup_method is not None:
|
||||||
|
|
Loading…
Reference in New Issue