]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-150114: Log the memory usage in regrtest (#150255)
authorVictor Stinner <vstinner@python.org>
Fri, 22 May 2026 17:05:45 +0000 (19:05 +0200)
committerGitHub <noreply@github.com>
Fri, 22 May 2026 17:05:45 +0000 (19:05 +0200)
On Linux, log the total memory usage of all Python test processes.
Read the private memory in /proc/pid/smaps.

Lib/test/libregrtest/logger.py
Lib/test/libregrtest/run_workers.py
Lib/test/libregrtest/utils.py
Lib/test/test_regrtest.py
Misc/NEWS.d/next/Tests/2026-05-22-16-41-45.gh-issue-150114.UdMikH.rst [new file with mode: 0644]

index fa1d4d575c8fd4f21e261f5a9c7518b77e20b1b6..4e011ef06f8a913669112d0f37903569f8f66894 100644 (file)
@@ -1,5 +1,6 @@
 import os
 import time
+from typing import Callable
 
 from test.support import MS_WINDOWS
 from .results import TestResults
@@ -19,16 +20,27 @@ class Logger:
         self._results: TestResults = results
         self._quiet: bool = quiet
         self._pgo: bool = pgo
+        self.get_mem_usage: Callable[[], int | None] | None = None
 
     def log(self, line: str = '') -> None:
         empty = not line
 
-        # add the system load prefix: "load avg: 1.80 "
+        # Add the memory usage: "mem: 1 GiB "
+        if self.get_mem_usage is not None:
+            mem = self.get_mem_usage()
+            if mem:
+                mib = mem / (1024*1024)
+                if mib >= 1024:
+                    line = f"mem: {mib / 1024:.1f} GiB {line}"
+                else:
+                    line = f"mem: {mib:.1f} MiB {line}"
+
+        # Add the system load prefix: "load avg: 1.80 "
         load_avg = self.get_load_avg()
         if load_avg is not None:
             line = f"load avg: {load_avg:.2f} {line}"
 
-        # add the timestamp prefix:  "0:01:05 "
+        # Add the timestamp prefix:  "0:01:05 "
         log_time = time.perf_counter() - self.start_time
 
         mins, secs = divmod(int(log_time), 60)
index 424085a0050eb59e43b7b9a3c1b7a89b31cb0f6a..befdac7ee77f10705f3242b2f1703952b1278bbb 100644 (file)
@@ -22,7 +22,7 @@ from .runtests import RunTests, WorkerRunTests, JsonFile, JsonFileType
 from .single import PROGRESS_MIN_TIME
 from .utils import (
     StrPath, TestName,
-    format_duration, print_warning, count, plural)
+    format_duration, print_warning, count, plural, get_process_memory_usage)
 from .worker import create_worker_process, USE_PROCESS_GROUP
 
 if MS_WINDOWS:
@@ -452,6 +452,12 @@ class WorkerThread(threading.Thread):
                 print_warning(f"Failed to join {self} in {format_duration(dt)}")
                 break
 
+    def get_mem_usage(self):
+        popen = self._popen
+        if popen is None:
+            return
+        return get_process_memory_usage(popen.pid)
+
 
 def get_running(workers: list[WorkerThread]) -> str | None:
     running: list[str] = []
@@ -473,6 +479,7 @@ class RunWorkers:
                  logger: Logger, results: TestResults) -> None:
         self.num_workers = num_workers
         self.runtests = runtests
+        self.logger = logger
         self.log = logger.log
         self.display_progress = logger.display_progress
         self.results: TestResults = results
@@ -598,9 +605,21 @@ class RunWorkers:
 
         return result
 
+    def get_mem_usage(self):
+        usage = 0
+        main_mem = get_process_memory_usage(os.getpid())
+        if main_mem:
+            usage += main_mem
+        for worker in self.workers:
+            worker_mem = worker.get_mem_usage()
+            if worker_mem:
+                usage += worker_mem
+        return usage
+
     def run(self) -> None:
         fail_fast = self.runtests.fail_fast
         fail_env_changed = self.runtests.fail_env_changed
+        self.logger.get_mem_usage = self.get_mem_usage
 
         self.start_workers()
 
@@ -625,3 +644,4 @@ class RunWorkers:
             # worker when we exit this function
             self.pending.stop()
             self.stop_workers()
+            self.logger.get_mem_usage = None
index 00703d6c074855bbdbc1fef65f54861c4a339ead..1b4cb96406d6f604375a1da8b64885299f6a3418 100644 (file)
@@ -752,3 +752,26 @@ def display_title(title):
     print(title)
     print("#" * len(title))
     print(flush=True)
+
+
+def get_process_memory_usage(pid: int) -> int | None:
+    """
+    Read the private memory in bytes from /proc/pid/smaps.
+    """
+    try:
+        fp = open(f"/proc/{pid}/smaps", "rb")
+    except OSError:
+        return None
+
+    try:
+        total = 0
+        with fp:
+            for line in fp:
+                # Include both Private_Clean and Private_Dirty sections.
+                line = line.rstrip()
+                if line.startswith(b"Private_") and line.endswith(b'kB'):
+                    parts = line.split()
+                    total += int(parts[1]) * 1024
+        return total
+    except ProcessLookupError:
+        return None
index 02f6e0c74b5ce84be731a0c958f7d08d1dbb7276..207b144d01d92559c58b575e6ee87d88a0942d9a 100644 (file)
@@ -41,7 +41,11 @@ if not support.has_subprocess_support:
 
 ROOT_DIR = os.path.join(os.path.dirname(__file__), '..', '..')
 ROOT_DIR = os.path.abspath(os.path.normpath(ROOT_DIR))
-LOG_PREFIX = r'[0-9]+:[0-9]+:[0-9]+ (?:load avg: [0-9]+\.[0-9]{2} )?'
+LOG_PREFIX = (
+    r'[0-9]+:[0-9]+:[0-9]+ '
+    r'(?:load avg: [0-9]+\.[0-9]{2} )?'
+    r'(?:mem: [0-9]+\.[0-9] (?:MiB|GiB) )?'
+)
 RESULT_REGEX = (
     'passed',
     'failed',
diff --git a/Misc/NEWS.d/next/Tests/2026-05-22-16-41-45.gh-issue-150114.UdMikH.rst b/Misc/NEWS.d/next/Tests/2026-05-22-16-41-45.gh-issue-150114.UdMikH.rst
new file mode 100644 (file)
index 0000000..a140bf9
--- /dev/null
@@ -0,0 +1,2 @@
+On Linux, regrtest now logs the total memory usage of all Python processes.
+Read the private memory in ``/proc/pid/smaps``. Patch by Victor Stinner.