Coverage for pyTooling/Process/__init__.py: 86%
91 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-03 23:02 +0000
1# ==================================================================================================================== #
2# _____ _ _ ____ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | _ \ _ __ ___ ___ ___ ___ ___ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |_) | '__/ _ \ / __/ _ \/ __/ __| #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| __/| | | (_) | (_| __/\__ \__ \ #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_| |_| \___/ \___\___||___/___/ #
7# |_| |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32An operating system independent abstraction of the currently running process.
34The process' properties are queried through one API, whichever operating system provides them, so a program reading
35its own memory usage needs no platform handling of its own.
37.. seealso::
39 :mod:`pyTooling.Platform`
40 |rarr| The platform this process runs on.
41 :mod:`pyTooling.Stopwatch`
42 |rarr| Measuring how long a piece of code took, next to how much memory it used.
43"""
44from ctypes import Structure, c_void_p, c_size_t, c_int, c_int32, c_uint64
45from os import getpid, strerror
46from pathlib import Path
47from typing import ClassVar, Any
49from pyTooling.Decorators import export, readonly
50from pyTooling.MetaClasses import ExtendedType
51from pyTooling.Platform import PlatformError, CurrentPlatform
53if CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment:
54 from ctypes import WinDLL
55 from ctypes.wintypes import HANDLE, BOOL, DWORD
58@export
59class MemoryInfo(metaclass=ExtendedType, slots=True):
60 """A snapshot of a process' memory usage: physically mapped pages and total virtual address space."""
62 _ResidentMemory: int #: Resident Set Size (VmRSS) – physical pages currently mapped. Memory usage in bytes.
63 _VirtualMemory: int #: Virtual Memory Size (VmS) – total virtual address space used. Memory usage in bytes.
65 def __init__(self, residentMemory: int, virtualMemory: int) -> None:
66 """
67 Initializes the memory info object with **Resident Set Size** and **Virtual Memory Size**.
69 :param residentMemory: Resident Memory Size (VmRSS) in bytes.
70 :param virtualMemory: Virtual Memory Size (VmS) in bytes.
71 """
72 self._ResidentMemory = residentMemory
73 self._VirtualMemory = virtualMemory
75 @readonly
76 def ResidentMemory(self) -> int:
77 """
78 Read-only property to access the **Resident Set Size** (used physical memory).
80 :returns: Resident Set Size (VmRSS) in bytes.
81 """
82 return self._ResidentMemory
84 @readonly
85 def VirtualMemory(self) -> int:
86 """
87 Read-only property to access the **Virtual Memory Size** (used virtual memory).
89 :returns: Virtual Memory Size (VmS) in bytes.
90 """
91 return self._VirtualMemory
93 def __str__(self) -> str:
94 """
95 Return a string representation of this memory snapshot.
97 :returns: Resident and virtual memory usage in MiB.
98 """
99 return f"Physical Memory (VmRSS): {self.ResidentMemory / 2**20:.3f} MiB / Virtual Memory (VmS): {self.VirtualMemory / 2**20:.3f} MiB"
102@export
103class ProcessInformation(metaclass=ExtendedType, slots=True):
104 """
105 Access to the current process' information, implemented per platform.
107 Windows reads it through ``psapi``, Linux through :file:`/proc/self/statm` and macOS through ``proc_pidinfo``, so
108 the class body itself differs by platform while :attr:`MemoryInfo` is the same everywhere.
109 """
111 if CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment:
112 _psapi: WinDLL
113 _kernel32: WinDLL
114 _processHandle: Any
115 elif CurrentPlatform.IsNativeLinux:
116 _processStatusFile: ClassVar[Path] = Path(f"/proc/self/statm")
117 elif CurrentPlatform.IsNativeFreeBSD: 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true
118 pass
120 if CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment:
121 def __init__(self) -> None:
122 """
123 Initialize the process information by opening the Windows libraries it queries.
125 :attr:`_psapi` and :attr:`_kernel32` are loaded, ``GetCurrentProcess`` is declared, and the handle it returns
126 is kept in :attr:`_processHandle` for the lifetime of this object.
127 """
128 self._psapi = WinDLL("psapi", use_last_error=True)
129 self._kernel32 = WinDLL("kernel32", use_last_error=True)
131 self._kernel32.GetCurrentProcess.restype = HANDLE
132 self._kernel32.GetCurrentProcess.argtypes = []
134 self._processHandle = self._kernel32.GetCurrentProcess()
135 else:
136 def __init__(self) -> None:
137 """
138 Initialize the process information.
140 There is nothing to open outside Windows: :attr:`_psapi`, :attr:`_kernel32` and :attr:`_processHandle` are
141 declared under the same platform condition as this initializer, so they don't exist here.
142 """
143 pass
145 if CurrentPlatform.IsNativeLinux:
146 from os import sysconf
147 _pageSize: ClassVar[int] = sysconf("SC_PAGESIZE")
149 def GetMemoryUsage(self) -> MemoryInfo:
150 """
151 Get the memory usage of this Python process on a Linux system.
153 Read the `/proc/self/statm` memory statistic file (space separated) for the current process:
155 [0] size
156 VmSize (total virtual address space)
157 [1] resident
158 VmRSS - Virtual memory Resident Set Size (pages currently resident in RAM) = used physical memory
159 [2] shared
160 shared pages (mapped from files)
161 [3] text
162 code segment pages
163 [4] lib
164 unused (always 0 since Linux 2.6)
165 [5] data
166 data + stack pages
167 [6] dt
168 dirty pages (always 0 since Linux 2.6)
170 ``SC_PAGESIZE`` is typically 4096 bytes, but can be 16kiB (ARM64) or 64kiB (PowerPC/RHEL9+). :func:`os.sysconf`
171 reads it from the aux vector — no syscall overhead.
173 :returns: Physical memory usage (VmRSS) in bytes.
174 :raises PlatformError: If the process' memory usage couldn't be read.
175 """
177 try:
178 with self._processStatusFile.open("rb") as f:
179 fields = f.read().split()
180 except FileNotFoundError as ex:
181 raise PlatformError(f"Can't open '{self._processStatusFile}' to extract the process' physical memory usage.") from ex
183 vms = int(fields[0]) * self._pageSize #: VmSize
184 rss = int(fields[1]) * self._pageSize #: VmRSS
186 return MemoryInfo(rss, vms)
188 elif CurrentPlatform.IsNativeFreeBSD: 188 ↛ 189line 188 didn't jump to line 189 because the condition on line 188 was never true
189 def GetMemoryUsage(self) -> MemoryInfo:
190 """
191 Get the memory usage of this Python process on a FreeBSD system.
193 ``resource.getrusage`` provides the resident size portably on FreeBSD.
194 Virtual memory usage is not exposed by the stdlib here, so report ``0``
195 for the virtual component until upstream adds a native implementation.
197 :returns: Memory usage of the current process.
198 """
199 from resource import RUSAGE_SELF, getrusage
201 rss = getrusage(RUSAGE_SELF).ru_maxrss * 1024
202 return MemoryInfo(rss, 0)
204 elif CurrentPlatform.IsNativeMacOS:
205 class _ProcTaskInfo(Structure):
206 """
207 ``struct proc_taskinfo`` from ``<sys/proc_info.h>``
208 """
209 _fields_ = [
210 ("pti_virtual_size", c_uint64),
211 ("pti_resident_size", c_uint64),
212 ("pti_total_user", c_uint64),
213 ("pti_total_system", c_uint64),
214 ("pti_threads_user", c_uint64),
215 ("pti_threads_system", c_uint64),
216 ("pti_policy", c_int32),
217 ("pti_faults", c_int32),
218 ("pti_pageins", c_int32),
219 ("pti_cow_faults", c_int32),
220 ("pti_messages_sent", c_int32),
221 ("pti_messages_received", c_int32),
222 ("pti_syscalls_mach", c_int32),
223 ("pti_syscalls_unix", c_int32),
224 ("pti_csw", c_int32),
225 ("pti_threadnum", c_int32),
226 ("pti_numrunning", c_int32),
227 ("pti_priority", c_int32),
228 ]
230 def GetMemoryUsage(self) -> MemoryInfo:
231 """
232 Call libproc.proc_pidinfo(PROC_PIDTASKINFO) – the same route psutil takes.
234 struct proc_taskinfo (<sys/proc_info.h>):
235 pti_virtual_size uint64 – virtual address space in bytes
236 pti_resident_size uint64 – resident (physical) memory in bytes
237 … 16 further fields (timing, policy, fault/syscall counters)
239 proc_pidinfo() returns the number of bytes written; ≤ 0 means error
240 (errno is set). PROC_PIDTASKINFO = 4.
242 :returns: Memory usage of the current process.
243 :raises PlatformError: If ``proc_pidinfo`` reported an error.
244 """
245 from ctypes import CDLL, byref, sizeof, get_errno
246 from ctypes.util import find_library
248 PROC_PIDTASKINFO = 4
250 _libproc_path = find_library("proc") # or "/usr/lib/libproc.dylib"
251 _libproc = CDLL(_libproc_path, use_errno=True)
252 _libproc.proc_pidinfo.restype = c_int
253 _libproc.proc_pidinfo.argtypes = [
254 c_int, # pid
255 c_int, # flavor
256 c_uint64, # arg (unused for PROC_PIDTASKINFO)
257 c_void_p, # buffer
258 c_int, # buffersize
259 ]
261 taskInfo = self._ProcTaskInfo()
262 ret = _libproc.proc_pidinfo(getpid(), PROC_PIDTASKINFO, 0, byref(taskInfo), sizeof(taskInfo))
263 if ret <= 0: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true
264 err = get_errno()
265 raise PlatformError("Failed to get current process' information.") from OSError(err, strerror(err), "proc_pidinfo")
267 return MemoryInfo(taskInfo.pti_resident_size, taskInfo.pti_virtual_size)
269 elif CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment: 269 ↛ 320line 269 didn't jump to line 320 because the condition on line 269 was always true
270 class _ProcessMemoryCounters(Structure):
271 """The Windows ``PROCESS_MEMORY_COUNTERS`` structure, as filled in by ``GetProcessMemoryInfo``."""
273 from ctypes.wintypes import DWORD
275 _fields_ = [
276 ("cb", DWORD),
277 ("PageFaultCount", DWORD),
278 ("PeakWorkingSetSize", c_size_t),
279 ("WorkingSetSize", c_size_t),
280 ("QuotaPeakPagedPoolUsage", c_size_t),
281 ("QuotaPagedPoolUsage", c_size_t),
282 ("QuotaPeakNonPagedPoolUsage", c_size_t),
283 ("QuotaNonPagedPoolUsage", c_size_t),
284 ("PagefileUsage", c_size_t),
285 ("PeakPagefileUsage", c_size_t),
286 ]
288 del DWORD
290 def GetMemoryUsage(self) -> MemoryInfo:
291 """
292 Call psapi.GetProcessMemoryInfo() with a PROCESS_MEMORY_COUNTERS struct.
294 WorkingSetSize – physical pages currently mapped → RSS
295 PagefileUsage – private committed bytes → VMS (= "Private Bytes"
296 in Task Manager; mirrors psutil's vms on Windows)
298 GetCurrentProcess() returns a pseudo-handle (-1) requiring no CloseHandle.
299 use_last_error=True routes SetLastError / GetLastError through ctypes so
300 WinError() picks up the correct code without a race.
302 :returns: Memory usage of the current process.
303 :raises WinError: If ``GetProcessMemoryInfo`` reported an error.
304 """
306 from ctypes import WinDLL, WinError, POINTER, sizeof, byref, get_last_error
308 self._psapi.GetProcessMemoryInfo.restype = BOOL
309 self._psapi.GetProcessMemoryInfo.argtypes = [HANDLE, POINTER(self._ProcessMemoryCounters), DWORD]
311 processMemoryCounters = self._ProcessMemoryCounters()
312 processMemoryCounters.cb = sizeof(processMemoryCounters)
314 if not self._psapi.GetProcessMemoryInfo(self._processHandle, byref(processMemoryCounters), processMemoryCounters.cb): 314 ↛ 315line 314 didn't jump to line 315 because the condition on line 314 was never true
315 raise WinError(get_last_error())
317 return MemoryInfo(processMemoryCounters.WorkingSetSize, processMemoryCounters.PagefileUsage)
319 else:
320 raise PlatformError(f"Unsupported platform: '{CurrentPlatform}'.")