Skip to content

Commit a1aa01e

Browse files
gh-75876: Report the peak memory use of a bigmem test
In verbose mode, report how much memory the test really used, next to how much it declared. The subprocess measures its own peak; if it died before reporting it, the parent reports what it sampled, as a lower bound. Report also the number of major page faults, if any: the test did not fit in memory, so its timing means little. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 66d7c89 commit a1aa01e

3 files changed

Lines changed: 65 additions & 3 deletions

File tree

Lib/test/support/__init__.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,15 +1271,20 @@ def set_memlimit(limit: str) -> None:
12711271

12721272

12731273
def _memory_watchdog(pid):
1274-
"""Return a function printing the memory usage of process *pid*."""
1274+
"""Return a function printing the memory usage of process *pid*.
1275+
1276+
The largest value it saw is kept in its ``peak`` attribute.
1277+
"""
12751278
# Imported here: test.support does not depend on test.libregrtest.
12761279
from test.libregrtest.utils import get_process_memory_usage
12771280

12781281
def watch():
12791282
mem = get_process_memory_usage(pid)
12801283
if mem is not None:
1284+
watch.peak = max(watch.peak, mem)
12811285
print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB",
12821286
flush=True)
1287+
watch.peak = 0
12831288
return watch
12841289

12851290

@@ -1333,7 +1338,23 @@ def wrapper(self):
13331338
qualname = f'{cls.__qualname__}.{f.__name__}'
13341339
proc = isolation._start_test(cls.__module__, qualname)
13351340
watchdog = _memory_watchdog(proc.pid) if verbose else None
1336-
isolation._replay_test(self, *proc.wait(tick=watchdog))
1341+
payload, output, returncode = proc.wait(tick=watchdog)
1342+
if watchdog:
1343+
# The subprocess measures its own peak exactly. What the
1344+
# parent sampled is only a lower bound.
1345+
maxrss = payload and payload.get('maxrss')
1346+
peak = maxrss or watchdog.peak
1347+
if peak:
1348+
print(f" ... peak memory use: "
1349+
f"{peak / (1024 ** 3):.1f} GiB"
1350+
f"{'' if maxrss else ' or more'}", flush=True)
1351+
majflt = payload and payload.get('majflt')
1352+
if majflt:
1353+
# The test did not fit in memory, so its timing means
1354+
# little.
1355+
print(f" ... {majflt} major page faults: the test "
1356+
f"waited for the disk", flush=True)
1357+
isolation._replay_test(self, payload, output, returncode)
13371358
return
13381359

13391360
return f(self, maxsize)

Lib/test/support/subprocess_runner.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,45 @@ def _outcome(kind, test, detail):
7272
for t, tb in result.expectedFailures]
7373
outcomes += [_outcome('skipped', t, reason) for t, reason in result.skipped]
7474

75-
payload = {'outcomes': outcomes, 'durations': result.id_durations}
75+
def _usage():
76+
"""What this process used: peak resident set size in bytes, and the
77+
number of major page faults it took, either of which can be None.
78+
79+
A major page fault is served from disk, so a non-zero count means swapping.
80+
81+
The modules are imported here, after the test has run, so that the test
82+
does not see them.
83+
"""
84+
try:
85+
import resource
86+
except ImportError:
87+
pass
88+
else:
89+
usage = resource.getrusage(resource.RUSAGE_SELF)
90+
# Solaris and illumos leave these fields at 0, which no live process
91+
# has, so treat it as "not supported".
92+
if not usage.ru_maxrss:
93+
return {'maxrss': None, 'majflt': None}
94+
# ru_maxrss is in bytes on macOS, in kilobytes on Linux and the BSDs.
95+
maxrss = usage.ru_maxrss
96+
return {'maxrss': maxrss if sys.platform == 'darwin' else maxrss * 1024,
97+
'majflt': usage.ru_majflt}
98+
try:
99+
import os
100+
import _winapi
101+
handle = _winapi.OpenProcess(
102+
_winapi.PROCESS_QUERY_LIMITED_INFORMATION, False, os.getpid())
103+
except (ImportError, OSError):
104+
return {'maxrss': None, 'majflt': None}
105+
try:
106+
info = _winapi.GetProcessMemoryInfo(handle)
107+
finally:
108+
_winapi.CloseHandle(handle)
109+
# PageFaultCount counts all faults, not only the ones served from disk.
110+
return {'maxrss': info['PeakWorkingSetSize'], 'majflt': None}
111+
112+
113+
payload = {'outcomes': outcomes, 'durations': result.id_durations, **_usage()}
76114
with open(outfile, 'wb') as f:
77115
marshal.dump(payload, f)
78116

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In verbose mode, a test decorated with :func:`~test.support.bigmemtest` now
2+
reports how much memory it really used, and the number of major page faults
3+
it took, if any.

0 commit comments

Comments
 (0)