From ec8400fa9b38e6d961a5cf585a03c9c1a8f6a898 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 1 Nov 2026 16:23:59 -0500 Subject: [PATCH] ci(unit-tests): split the PyPy protocols shard so its tail stops stalling (#2600) - unit-tests.yml: a PyPy leg's SHARDS runs tests/protocols/internet, application and misc on their own ahead of tests/protocols, which then takes the remainder; test-ktlo shares the step by YAML alias. Run whole, tests/protocols peaked at 17.7 GiB at -n 4 against the runner's 16 GB. - test_pypy_worker_memory_2005: pin the new SHARDS, and require each shard to keep unit-tier modules no earlier shard took. - workflows.rst: name the shards and record the measurement. Collect-only on PyPy 7.3.19: same 2717 ids, none duplicated; the split protocols runs peak at 5.7 GiB and take 224 s against 267 s. --- .github/workflows/unit-tests.yml | 7 ++++++- docs/source/contributing/workflows.rst | 15 ++++++++++++--- tests/project/test_pypy_worker_memory_2005.py | 16 ++++++++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 86748b46de..1f15504724 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -205,10 +205,15 @@ jobs: # pytest once per directory `SHARDS` names, then once over the rest, # and every run starts its workers afresh. Each run but the last writes # a JUnit file of its own, beside junit.xml; the step fails if any did. + # + # #2600: run whole, tests/protocols took the workers past the runner's + # 16 GB, and its tail stalled past tests._support.time_limit's 30 s. So + # its internet, application and misc subpackages run first, each a shard + # of its own, and `tests/protocols` then takes the remainder. env: COVERAGE: ${{ matrix.coverage }} LATEST: ${{ matrix.python-version == needs.python-versions.outputs.latest }} - SHARDS: ${{ startsWith(matrix.python-version, 'pypy') && 'tests/protocols tests/corekit' || '' }} + SHARDS: ${{ startsWith(matrix.python-version, 'pypy') && 'tests/protocols/internet tests/protocols/application tests/protocols/misc tests/protocols tests/corekit' || '' }} run: | if [ "$LATEST" = "true" ]; then if ! skipped=$(python -c "from tests._tiers import project_pin_paths; print(' '.join(project_pin_paths()))"); then diff --git a/docs/source/contributing/workflows.rst b/docs/source/contributing/workflows.rst index 520654e75b..4cf70efc97 100644 --- a/docs/source/contributing/workflows.rst +++ b/docs/source/contributing/workflows.rst @@ -1046,12 +1046,21 @@ extras cap |cryptography|_ below 46, as 45.0.7 is the newest release with a Linu On PyPy an xdist worker's RSS grows with every test it runs, rather than in a few heavy modules, so a PyPy leg's ``Run unit tests`` hands its pytest command to :file:`util/pytest_shards.py`, as GraalPy's leg does (see below), which runs it once per -directory its ``SHARDS`` names, ``tests/protocols`` and then ``tests/corekit``, and once -more over the rest, ignoring each (:issue:`2005`). +directory its ``SHARDS`` names and once more over the rest, ignoring each +(:issue:`2005`). The directories are ``tests/protocols/internet``, +``tests/protocols/application``, ``tests/protocols/misc``, ``tests/protocols`` and +``tests/corekit``, in that order, so ``tests/protocols`` runs after its own +subpackages and takes only what they left. Each run starts its workers afresh. Measured on PyPy 8.0.1 at ``-n 4``, one run over the whole ``ALL`` tier took each worker to 5.4-5.9 GB and the workers together past 20 GB, beyond the runner's 16 GB; split, the largest run peaked at 2.6 GB a -worker and 8.5 GB in all, and the tier took 383 s rather than 493 s. A smaller +worker and 8.5 GB in all, and the tier took 383 s rather than 493 s. With +:issue:`2543`'s checksum tests, ``tests/protocols`` run whole outgrew the runner as +well, and on CI the tests at its tail stalled past ``tests._support.time_limit``'s +30-second deadline, which counts wall-clock time, so it is split (:issue:`2600`). +Measured on PyPy 7.3.19 at ``-n 4``, its one run peaked at 17.7 GiB and took 267 s, +its slowest test 16.4 s; split, the largest of its runs peaked at 5.7 GiB, they took +224 s together, and the slowest test 11.9 s. A smaller ``-n`` would only hand each worker more tests, and a ``PYPY_GC_MAX`` heap cap turns the kills into ``MemoryError``\ s. Each run but the last writes a JUnit file of its own beside ``junit.xml``, which ``Upload test timings`` takes too, and the step diff --git a/tests/project/test_pypy_worker_memory_2005.py b/tests/project/test_pypy_worker_memory_2005.py index 32614a7f73..cb76f87682 100644 --- a/tests/project/test_pypy_worker_memory_2005.py +++ b/tests/project/test_pypy_worker_memory_2005.py @@ -9,6 +9,10 @@ own script with a stub ``python``, as :mod:`tests.project.test_sweeps_on_latest_only_1732` does. +#2600: run whole, ``tests/protocols`` outgrew the runner too, so its +``internet``, ``application`` and ``misc`` subpackages are shards of their own +ahead of it, and it takes the remainder. + """ from __future__ import annotations @@ -33,8 +37,9 @@ #: The ``test`` job's KTLO twin, which runs its steps by YAML alias. KTLO = ('unit-tests.yml', 'test-ktlo') -#: The directories a PyPy leg runs on their own, in order. -SHARDS = ['tests/protocols', 'tests/corekit'] +#: The directories a PyPy leg runs on their own, in order; #2600 split ``tests/protocols``. +SHARDS = ['tests/protocols/internet', 'tests/protocols/application', 'tests/protocols/misc', 'tests/protocols', + 'tests/corekit'] #: The page that says how the legs run. DOC = ROOT / 'docs' / 'source' / 'contributing' / 'workflows.rst' @@ -61,10 +66,13 @@ def test_the_ktlo_twin_runs_the_same_step(self) -> None: self.assertEqual(job(*KTLO)['steps'], job(*JOB)['steps']) def test_each_shard_is_a_directory_the_tier_keeps(self) -> None: - for shard in SHARDS: + """#2600: and each keeps unit-tier modules no earlier shard took, so its run collects a test.""" + for index, shard in enumerate(SHARDS): with self.subTest(shard=shard): self.assertTrue((ROOT / shard).is_dir()) - self.assertTrue([module for module in _tiers.all_tier_paths() if module.startswith(shard + '/')]) + earlier = tuple(taken + '/' for taken in SHARDS[:index]) + self.assertTrue([module for module in _tiers.all_tier_paths() if module.startswith(shard + '/') + and not module.startswith(earlier) and _tiers.is_unit_tier(module)]) @unittest.skipIf(shutil.which('bash') is None, 'the step is a bash script')