Skip to content

Commit 9043fa5

Browse files
gh-75876: Limit the address space of a bigmem test
A test run with -M now runs in a subprocess whose address space is limited to what it declares plus 512 MiB. A test which uses much more memory than it declares fails with a MemoryError instead of making the machine swap. Pass limit_address_space=False for a test which reserves much more address space than it uses. The two threaded tests in test_interpreters do: glibc reserves an arena per thread, up to 8 per CPU, and this alone exceeds the declared size on a machine with many cores.
1 parent 66d7c89 commit 9043fa5

4 files changed

Lines changed: 49 additions & 4 deletions

File tree

Doc/library/test.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ The :mod:`!test.support` module defines the following functions:
835835
the trace function.
836836

837837

838-
.. decorator:: bigmemtest(size, memuse, dry_run=True)
838+
.. decorator:: bigmemtest(size, memuse, dry_run=True, *, limit_address_space=True)
839839

840840
Decorator for bigmem tests.
841841

@@ -849,6 +849,12 @@ The :mod:`!test.support` module defines the following functions:
849849
method may be less than the requested value. If *dry_run* is ``False``, it
850850
means the test doesn't support dummy runs when ``-M`` is not specified.
851851

852+
A test which really allocates the memory it asks for runs in a separate
853+
process, whose address space is limited to what the test declares plus a
854+
margin.
855+
Set *limit_address_space* to ``False`` for a test which reserves much more
856+
address space than it uses, for example one which starts many threads.
857+
852858

853859
.. decorator:: bigaddrspacetest
854860

Lib/test/support/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,27 @@ def set_memlimit(limit: str) -> None:
12701270
max_memuse = memlimit
12711271

12721272

1273+
def _limit_address_space(nbytes):
1274+
"""Limit the address space of this process to *nbytes* plus a margin.
1275+
1276+
A test which uses much more memory than it declares then fails with a
1277+
MemoryError instead of making the machine swap.
1278+
"""
1279+
try:
1280+
import resource
1281+
rlimit = resource.RLIMIT_AS
1282+
except (ImportError, AttributeError):
1283+
return
1284+
# The margin does not grow with the declared size: the interpreter itself
1285+
# reserves about 250 MiB, whatever the test asks for.
1286+
limit = int(nbytes) + 512 * _1M
1287+
soft, hard = resource.getrlimit(rlimit)
1288+
for current in soft, hard:
1289+
if current != resource.RLIM_INFINITY:
1290+
limit = min(limit, current)
1291+
resource.setrlimit(rlimit, (limit, hard))
1292+
1293+
12731294
def _memory_watchdog(pid):
12741295
"""Return a function printing the memory usage of process *pid*."""
12751296
# Imported here: test.support does not depend on test.libregrtest.
@@ -1283,7 +1304,7 @@ def watch():
12831304
return watch
12841305

12851306

1286-
def bigmemtest(size, memuse, dry_run=True):
1307+
def bigmemtest(size, memuse, dry_run=True, *, limit_address_space=True):
12871308
"""Decorator for bigmem tests.
12881309
12891310
'size' is a requested size for the test (in arbitrary, test-interpreted
@@ -1299,6 +1320,12 @@ def bigmemtest(size, memuse, dry_run=True):
12991320
A test that actually allocates the requested memory (that is, one run with
13001321
-M) runs in a subprocess, so that the memory it uses and the address space
13011322
it fragments are released when it ends. A dummy run stays in the process.
1323+
1324+
The address space of that subprocess is limited to what the test declares
1325+
plus a margin, so that a test which uses much more memory than it declares
1326+
fails instead of making the machine swap. Pass 'limit_address_space' as
1327+
false for a test which reserves much more address space than it uses, for
1328+
example one which starts many threads.
13021329
"""
13031330
def decorator(f):
13041331
from test.support import isolation
@@ -1336,6 +1363,11 @@ def wrapper(self):
13361363
isolation._replay_test(self, *proc.wait(tick=watchdog))
13371364
return
13381365

1366+
if (real_max_memuse and limit_address_space
1367+
and isolation.runningInSubprocess):
1368+
# Only in the subprocess: the limit is never lifted.
1369+
_limit_address_space(size * memuse)
1370+
13391371
return f(self, maxsize)
13401372

13411373
wrapper.size = size

Lib/test/test_interpreters/test_stress.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ def test_create_many_sequential(self):
2626
support.gc_collect()
2727

2828
@threading_helper.requires_working_threading()
29-
@support.bigmemtest(size=200, memuse=32*2**20, dry_run=False)
29+
@support.bigmemtest(size=200, memuse=32*2**20, dry_run=False,
30+
limit_address_space=False)
3031
def test_create_many_threaded(self, size):
3132
alive = []
3233
start = threading.Event()
@@ -43,7 +44,8 @@ def task():
4344
support.gc_collect()
4445

4546
@threading_helper.requires_working_threading()
46-
@support.bigmemtest(size=200, memuse=34*2**20, dry_run=False)
47+
@support.bigmemtest(size=200, memuse=34*2**20, dry_run=False,
48+
limit_address_space=False)
4749
def test_many_threads_running_interp_in_other_interp(self, size):
4850
start = threading.Event()
4951
interp = interpreters.create()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
A test decorated with :func:`~test.support.bigmemtest` now runs with the
2+
address space limited to what it declares plus 512 MiB, so that a test which
3+
uses much more memory than it declares fails instead of making the machine
4+
swap. Pass ``limit_address_space=False`` for a test which reserves much more
5+
address space than it uses.

0 commit comments

Comments
 (0)