Coverage for pyTooling/Process/__init__.py: 91%
84 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-22 21:29 +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 PlatformException, 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")
118 if CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment:
119 def __init__(self) -> None:
120 """
121 Initialize the process information by opening the Windows libraries it queries.
123 :attr:`_psapi` and :attr:`_kernel32` are loaded, ``GetCurrentProcess`` is declared, and the handle it returns
124 is kept in :attr:`_processHandle` for the lifetime of this object.
125 """
126 self._psapi = WinDLL("psapi", use_last_error=True)
127 self._kernel32 = WinDLL("kernel32", use_last_error=True)
129 self._kernel32.GetCurrentProcess.restype = HANDLE
130 self._kernel32.GetCurrentProcess.argtypes = []
132 self._processHandle = self._kernel32.GetCurrentProcess()
133 else:
134 def __init__(self) -> None:
135 """
136 Initialize the process information.
138 There is nothing to open outside Windows: :attr:`_psapi`, :attr:`_kernel32` and :attr:`_processHandle` are
139 declared under the same platform condition as this initializer, so they don't exist here.
140 """
141 pass
143 if CurrentPlatform.IsNativeLinux:
144 from os import sysconf
145 _pageSize: ClassVar[int] = sysconf("SC_PAGESIZE")
147 def GetMemoryUsage(self) -> MemoryInfo:
148 """
149 Get the memory usage of this Python process on a Linux system.
151 Read the `/proc/self/statm` memory statistic file (space separated) for the current process:
153 [0] size
154 VmSize (total virtual address space)
155 [1] resident
156 VmRSS - Virtual memory Resident Set Size (pages currently resident in RAM) = used physical memory
157 [2] shared
158 shared pages (mapped from files)
159 [3] text
160 code segment pages
161 [4] lib
162 unused (always 0 since Linux 2.6)
163 [5] data
164 data + stack pages
165 [6] dt
166 dirty pages (always 0 since Linux 2.6)
168 ``SC_PAGESIZE`` is typically 4096 bytes, but can be 16kiB (ARM64) or 64kiB (PowerPC/RHEL9+). :func:`os.sysconf`
169 reads it from the aux vector — no syscall overhead.
171 :returns: Physical memory usage (VmRSS) in bytes.
172 :raises PlatformException: If the process' memory usage couldn't be read.
173 """
175 try:
176 with self._processStatusFile.open("rb") as f:
177 fields = f.read().split()
178 except FileNotFoundError as ex:
179 raise PlatformException(f"Can't open '{self._processStatusFile}' to extract the process' physical memory usage.") from ex
181 vms = int(fields[0]) * self._pageSize #: VmSize
182 rss = int(fields[1]) * self._pageSize #: VmRSS
184 return MemoryInfo(rss, vms)
186 elif CurrentPlatform.IsNativeMacOS:
187 class _ProcTaskInfo(Structure):
188 """
189 ``struct proc_taskinfo`` from ``<sys/proc_info.h>``
190 """
191 _fields_ = [
192 ("pti_virtual_size", c_uint64),
193 ("pti_resident_size", c_uint64),
194 ("pti_total_user", c_uint64),
195 ("pti_total_system", c_uint64),
196 ("pti_threads_user", c_uint64),
197 ("pti_threads_system", c_uint64),
198 ("pti_policy", c_int32),
199 ("pti_faults", c_int32),
200 ("pti_pageins", c_int32),
201 ("pti_cow_faults", c_int32),
202 ("pti_messages_sent", c_int32),
203 ("pti_messages_received", c_int32),
204 ("pti_syscalls_mach", c_int32),
205 ("pti_syscalls_unix", c_int32),
206 ("pti_csw", c_int32),
207 ("pti_threadnum", c_int32),
208 ("pti_numrunning", c_int32),
209 ("pti_priority", c_int32),
210 ]
212 def GetMemoryUsage(self) -> MemoryInfo:
213 """
214 Call libproc.proc_pidinfo(PROC_PIDTASKINFO) – the same route psutil takes.
216 struct proc_taskinfo (<sys/proc_info.h>):
217 pti_virtual_size uint64 – virtual address space in bytes
218 pti_resident_size uint64 – resident (physical) memory in bytes
219 … 16 further fields (timing, policy, fault/syscall counters)
221 proc_pidinfo() returns the number of bytes written; ≤ 0 means error
222 (errno is set). PROC_PIDTASKINFO = 4.
224 :returns: Memory usage of the current process.
225 :raises PlatformException: If ``proc_pidinfo`` reported an error.
226 """
227 from ctypes import CDLL, byref, sizeof, get_errno
228 from ctypes.util import find_library
230 PROC_PIDTASKINFO = 4
232 _libproc_path = find_library("proc") # or "/usr/lib/libproc.dylib"
233 _libproc = CDLL(_libproc_path, use_errno=True)
234 _libproc.proc_pidinfo.restype = c_int
235 _libproc.proc_pidinfo.argtypes = [
236 c_int, # pid
237 c_int, # flavor
238 c_uint64, # arg (unused for PROC_PIDTASKINFO)
239 c_void_p, # buffer
240 c_int, # buffersize
241 ]
243 taskInfo = self._ProcTaskInfo()
244 ret = _libproc.proc_pidinfo(getpid(), PROC_PIDTASKINFO, 0, byref(taskInfo), sizeof(taskInfo))
245 if ret <= 0: 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true
246 err = get_errno()
247 raise PlatformException(f"Failed to get current process' information.") from OSError(err, strerror(err), "proc_pidinfo")
249 return MemoryInfo(taskInfo.pti_resident_size, taskInfo.pti_virtual_size)
251 elif CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment: 251 ↛ 302line 251 didn't jump to line 302 because the condition on line 251 was always true
252 class _ProcessMemoryCounters(Structure):
253 """The Windows ``PROCESS_MEMORY_COUNTERS`` structure, as filled in by ``GetProcessMemoryInfo``."""
255 from ctypes.wintypes import DWORD
257 _fields_ = [
258 ("cb", DWORD),
259 ("PageFaultCount", DWORD),
260 ("PeakWorkingSetSize", c_size_t),
261 ("WorkingSetSize", c_size_t),
262 ("QuotaPeakPagedPoolUsage", c_size_t),
263 ("QuotaPagedPoolUsage", c_size_t),
264 ("QuotaPeakNonPagedPoolUsage", c_size_t),
265 ("QuotaNonPagedPoolUsage", c_size_t),
266 ("PagefileUsage", c_size_t),
267 ("PeakPagefileUsage", c_size_t),
268 ]
270 del DWORD
272 def GetMemoryUsage(self) -> MemoryInfo:
273 """
274 Call psapi.GetProcessMemoryInfo() with a PROCESS_MEMORY_COUNTERS struct.
276 WorkingSetSize – physical pages currently mapped → RSS
277 PagefileUsage – private committed bytes → VMS (= "Private Bytes"
278 in Task Manager; mirrors psutil's vms on Windows)
280 GetCurrentProcess() returns a pseudo-handle (-1) requiring no CloseHandle.
281 use_last_error=True routes SetLastError / GetLastError through ctypes so
282 WinError() picks up the correct code without a race.
284 :returns: Memory usage of the current process.
285 :raises WinError: If ``GetProcessMemoryInfo`` reported an error.
286 """
288 from ctypes import WinDLL, WinError, POINTER, sizeof, byref, get_last_error
290 self._psapi.GetProcessMemoryInfo.restype = BOOL
291 self._psapi.GetProcessMemoryInfo.argtypes = [HANDLE, POINTER(self._ProcessMemoryCounters), DWORD]
293 processMemoryCounters = self._ProcessMemoryCounters()
294 processMemoryCounters.cb = sizeof(processMemoryCounters)
296 if not self._psapi.GetProcessMemoryInfo(self._processHandle, byref(processMemoryCounters), processMemoryCounters.cb): 296 ↛ 297line 296 didn't jump to line 297 because the condition on line 296 was never true
297 raise WinError(get_last_error())
299 return MemoryInfo(processMemoryCounters.WorkingSetSize, processMemoryCounters.PagefileUsage)
301 else:
302 raise PlatformException(f"Unsupported platform: '{CurrentPlatform}'.")