From 3fe877bc6d7211f31e55d2d561803f76ea95b935 Mon Sep 17 00:00:00 2001 From: Henk-Jaap Wagenaar Date: Tue, 21 Nov 2023 14:32:29 +0000 Subject: [PATCH] LoadScope scheduler: Sort scopes by number of tests to assign biggest scopes first (#778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow up to # 632. --------- Co-authored-by: Jörg Kohlsdorf --- changelog/632.feature.rst | 1 + src/xdist/scheduler/loadscope.py | 9 ++++++++- testing/acceptance_test.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 changelog/632.feature.rst diff --git a/changelog/632.feature.rst b/changelog/632.feature.rst new file mode 100644 index 0000000..eff46d4 --- /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 35a9ef3..bcfe11f 100644 --- a/src/xdist/scheduler/loadscope.py +++ b/src/xdist/scheduler/loadscope.py @@ -350,11 +350,18 @@ class LoadScopeScheduling: 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 60edd9c..7b83f02 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1232,6 +1232,22 @@ class TestLoadScope: "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 = """