diff --git a/changelog/632.feature.rst b/changelog/632.feature.rst new file mode 100644 index 00000000..eff46d40 --- /dev/null +++ b/changelog/632.feature.rst @@ -0,0 +1 @@ +``--dist=loadscope`` now sorts scopes by number of tests to assign largest scopes early -- in many cases this should improve overall test session running time, as there is less chance of a large scope being left to be processed near the end of the session, leaving other workers idle. diff --git a/src/xdist/scheduler/loadscope.py b/src/xdist/scheduler/loadscope.py index 35a9ef34..bcfe11fe 100644 --- a/src/xdist/scheduler/loadscope.py +++ b/src/xdist/scheduler/loadscope.py @@ -350,11 +350,18 @@ def schedule(self): return # Determine chunks of work (scopes) + unsorted_workqueue = OrderedDict() for nodeid in self.collection: scope = self._split_scope(nodeid) - work_unit = self.workqueue.setdefault(scope, default=OrderedDict()) + work_unit = unsorted_workqueue.setdefault(scope, default=OrderedDict()) work_unit[nodeid] = False + # Insert tests scopes into work queue ordered by number of tests. + for scope, nodeids in sorted( + unsorted_workqueue.items(), key=lambda item: -len(item[1]) + ): + self.workqueue[scope] = nodeids + # Avoid having more workers than work extra_nodes = len(self.nodes) - len(self.workqueue) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 60edd9cc..7b83f021 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1232,6 +1232,22 @@ def test(self, i): "test_a.py::TestB", result.outlines ) in ({"gw0": 10}, {"gw1": 10}) + def test_workqueue_ordered_by_size(self, pytester: pytest.Pytester) -> None: + test_file = """ + import pytest + @pytest.mark.parametrize('i', range({})) + def test(i): + pass + """ + pytester.makepyfile(test_a=test_file.format(10), test_b=test_file.format(20)) + result = pytester.runpytest("-n2", "--dist=loadscope", "-v") + assert get_workers_and_test_count_by_prefix( + "test_a.py::test", result.outlines + ) == {"gw1": 10} + assert get_workers_and_test_count_by_prefix( + "test_b.py::test", result.outlines + ) == {"gw0": 20} + def test_module_single_start(self, pytester: pytest.Pytester) -> None: """Fix test suite never finishing in case all workers start with a single test (#277).""" test_file1 = """