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

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

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 _pageSize: 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._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 

182 

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

184 rss = int(fields[1]) * self._pageSize #: 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 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) 

238 

239 proc_pidinfo() returns the number of bytes written; ≤ 0 means error 

240 (errno is set). PROC_PIDTASKINFO = 4. 

241 

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 

247 

248 PROC_PIDTASKINFO = 4 

249 

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 ] 

260 

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

266 

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

268 

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

272 

273 from ctypes.wintypes import DWORD 

274 

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 ] 

287 

288 del DWORD 

289 

290 def GetMemoryUsage(self) -> MemoryInfo: 

291 """ 

292 Call psapi.GetProcessMemoryInfo() with a PROCESS_MEMORY_COUNTERS struct. 

293 

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) 

297 

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. 

301 

302 :returns: Memory usage of the current process. 

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

304 """ 

305 

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

307 

308 self._psapi.GetProcessMemoryInfo.restype = BOOL 

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

310 

311 processMemoryCounters = self._ProcessMemoryCounters() 

312 processMemoryCounters.cb = sizeof(processMemoryCounters) 

313 

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()) 

316 

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

318 

319 else: 

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