Coverage for pyTooling/Process/__init__.py: 86%

91 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 12:33 +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. 

33 

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. 

36 

37.. seealso:: 

38 

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 

48 

49from pyTooling.Decorators import export, readonly 

50from pyTooling.MetaClasses import ExtendedType 

51from pyTooling.Platform import PlatformError, CurrentPlatform 

52 

53if CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment: 

54 from ctypes import WinDLL 

55 from ctypes.wintypes import HANDLE, BOOL, DWORD 

56 

57 

58@export 

59class MemoryInfo(metaclass=ExtendedType, slots=True): 

60 """A snapshot of a process' memory usage: physically mapped pages and total virtual address space.""" 

61 

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. 

64 

65 def __init__(self, residentMemory: int, virtualMemory: int) -> None: 

66 """ 

67 Initializes the memory info object with **Resident Set Size** and **Virtual Memory Size**. 

68 

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 

74 

75 @readonly 

76 def ResidentMemory(self) -> int: 

77 """ 

78 Read-only property to access the **Resident Set Size** (used physical memory). 

79 

80 :returns: Resident Set Size (VmRSS) in bytes. 

81 """ 

82 return self._ResidentMemory 

83 

84 @readonly 

85 def VirtualMemory(self) -> int: 

86 """ 

87 Read-only property to access the **Virtual Memory Size** (used virtual memory). 

88 

89 :returns: Virtual Memory Size (VmS) in bytes. 

90 """ 

91 return self._VirtualMemory 

92 

93 def __str__(self) -> str: 

94 """ 

95 Return a string representation of this memory snapshot. 

96 

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" 

100 

101 

102@export 

103class ProcessInformation(metaclass=ExtendedType, slots=True): 

104 """ 

105 Access to the current process' information, implemented per platform. 

106 

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 """ 

110 

111 if CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment: 

112 _psapi: WinDLL 

113 _kernel32: WinDLL 

114 _processHandle: Any 

115 elif CurrentPlatform.IsNativeLinux: 

116 _PROCESS_STATUS_FILE: ClassVar[Path] = Path("/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 

119 

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. 

124 

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) 

130 

131 self._kernel32.GetCurrentProcess.restype = HANDLE 

132 self._kernel32.GetCurrentProcess.argtypes = [] 

133 

134 self._processHandle = self._kernel32.GetCurrentProcess() 

135 else: 

136 def __init__(self) -> None: 

137 """ 

138 Initialize the process information. 

139 

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 

144 

145 if CurrentPlatform.IsNativeLinux: 

146 from os import sysconf 

147 _PAGE_SIZE: ClassVar[int] = sysconf("SC_PAGESIZE") 

148 

149 def GetMemoryUsage(self) -> MemoryInfo: 

150 """ 

151 Get the memory usage of this Python process on a Linux system. 

152 

153 Read the ``/proc/self/statm`` memory statistic file (space separated) for the current process: 

154 

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) 

169 

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. 

172 

173 :returns: Physical memory usage (VmRSS) in bytes. 

174 :raises PlatformError: If the process' memory usage couldn't be read. 

175 """ 

176 

177 try: 

178 with self._PROCESS_STATUS_FILE.open("rb") as f: 

179 fields = f.read().split() 

180 except FileNotFoundError as ex: 

181 raise PlatformError(f"Can't open '{self._PROCESS_STATUS_FILE}' to extract the process' physical memory usage.") from ex 

182 

183 vms = int(fields[0]) * self._PAGE_SIZE #: VmSize 

184 rss = int(fields[1]) * self._PAGE_SIZE #: VmRSS 

185 

186 return MemoryInfo(rss, vms) 

187 

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. 

192 

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. 

196 

197 :returns: Memory usage of the current process. 

198 """ 

199 from resource import RUSAGE_SELF, getrusage 

200 

201 rss = getrusage(RUSAGE_SELF).ru_maxrss * 1024 

202 return MemoryInfo(rss, 0) 

203 

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 ] 

229 

230 def GetMemoryUsage(self) -> MemoryInfo: 

231 """ 

232 Call libproc.proc_pidinfo(PROC_PIDTASKINFO) – the same route psutil takes. 

233 

234 ``struct proc_taskinfo`` (``<sys/proc_info.h>``): 

235 

236 * ``pti_virtual_size`` (``uint64``) - virtual address space in bytes 

237 * ``pti_resident_size`` (``uint64``) - resident (physical) memory in bytes 

238 * 16 further fields (timing, policy, fault/syscall counters) 

239 

240 ``proc_pidinfo()`` returns the number of bytes written; ``<= 0`` means error (``errno`` is set). 

241 ``PROC_PIDTASKINFO = 4``. 

242 

243 :returns: Memory usage of the current process. 

244 :raises PlatformError: If ``proc_pidinfo`` reported an error. 

245 """ 

246 from ctypes import CDLL, byref, sizeof, get_errno 

247 from ctypes.util import find_library 

248 

249 PROC_PIDTASKINFO = 4 

250 

251 _libproc_path = find_library("proc") # or "/usr/lib/libproc.dylib" 

252 _libproc = CDLL(_libproc_path, use_errno=True) 

253 _libproc.proc_pidinfo.restype = c_int 

254 _libproc.proc_pidinfo.argtypes = [ 

255 c_int, # pid 

256 c_int, # flavor 

257 c_uint64, # arg (unused for PROC_PIDTASKINFO) 

258 c_void_p, # buffer 

259 c_int, # buffersize 

260 ] 

261 

262 taskInfo = self._ProcTaskInfo() 

263 ret = _libproc.proc_pidinfo(getpid(), PROC_PIDTASKINFO, 0, byref(taskInfo), sizeof(taskInfo)) 

264 if ret <= 0: 264 ↛ 265line 264 didn't jump to line 265 because the condition on line 264 was never true

265 err = get_errno() 

266 raise PlatformError("Failed to get current process' information.") from OSError(err, strerror(err), "proc_pidinfo") 

267 

268 return MemoryInfo(taskInfo.pti_resident_size, taskInfo.pti_virtual_size) 

269 

270 elif CurrentPlatform.IsNativeWindows or CurrentPlatform.IsMSYS2Environment: 270 ↛ 321line 270 didn't jump to line 321 because the condition on line 270 was always true

271 class _ProcessMemoryCounters(Structure): 

272 """The Windows ``PROCESS_MEMORY_COUNTERS`` structure, as filled in by ``GetProcessMemoryInfo``.""" 

273 

274 from ctypes.wintypes import DWORD 

275 

276 _fields_ = [ 

277 ("cb", DWORD), 

278 ("PageFaultCount", DWORD), 

279 ("PeakWorkingSetSize", c_size_t), 

280 ("WorkingSetSize", c_size_t), 

281 ("QuotaPeakPagedPoolUsage", c_size_t), 

282 ("QuotaPagedPoolUsage", c_size_t), 

283 ("QuotaPeakNonPagedPoolUsage", c_size_t), 

284 ("QuotaNonPagedPoolUsage", c_size_t), 

285 ("PagefileUsage", c_size_t), 

286 ("PeakPagefileUsage", c_size_t), 

287 ] 

288 

289 del DWORD 

290 

291 def GetMemoryUsage(self) -> MemoryInfo: 

292 """ 

293 Call ``psapi.GetProcessMemoryInfo()`` with a ``PROCESS_MEMORY_COUNTERS`` struct. 

294 

295 * ``WorkingSetSize`` - physical pages currently mapped |rarr| RSS 

296 * ``PagefileUsage`` - private committed bytes |rarr| VMS (= "Private Bytes" in Task Manager; mirrors psutil's 

297 ``vms`` on Windows) 

298 

299 ``GetCurrentProcess()`` returns a pseudo-handle (``-1``) requiring no ``CloseHandle``. 

300 ``use_last_error=True`` routes ``SetLastError``/``GetLastError`` through :mod:`ctypes`, so ``WinError()`` 

301 picks up the correct code without a race. 

302 

303 :returns: Memory usage of the current process. 

304 :raises WinError: If ``GetProcessMemoryInfo`` reported an error. 

305 """ 

306 

307 from ctypes import WinDLL, WinError, POINTER, sizeof, byref, get_last_error 

308 

309 self._psapi.GetProcessMemoryInfo.restype = BOOL 

310 self._psapi.GetProcessMemoryInfo.argtypes = [HANDLE, POINTER(self._ProcessMemoryCounters), DWORD] 

311 

312 processMemoryCounters = self._ProcessMemoryCounters() 

313 processMemoryCounters.cb = sizeof(processMemoryCounters) 

314 

315 if not self._psapi.GetProcessMemoryInfo(self._processHandle, byref(processMemoryCounters), processMemoryCounters.cb): 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true

316 raise WinError(get_last_error()) 

317 

318 return MemoryInfo(processMemoryCounters.WorkingSetSize, processMemoryCounters.PagefileUsage) 

319 

320 else: 

321 raise PlatformError(f"Unsupported platform: '{CurrentPlatform}'.")