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

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 PlatformException, 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 

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. 

122 

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) 

128 

129 self._kernel32.GetCurrentProcess.restype = HANDLE 

130 self._kernel32.GetCurrentProcess.argtypes = [] 

131 

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

133 else: 

134 def __init__(self) -> None: 

135 """ 

136 Initialize the process information. 

137 

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 

142 

143 if CurrentPlatform.IsNativeLinux: 

144 from os import sysconf 

145 _pageSize: ClassVar[int] = sysconf("SC_PAGESIZE") 

146 

147 def GetMemoryUsage(self) -> MemoryInfo: 

148 """ 

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

150 

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

152 

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) 

167 

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. 

170 

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

172 :raises PlatformException: If the process' memory usage couldn't be read. 

173 """ 

174 

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 

180 

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

182 rss = int(fields[1]) * self._pageSize #: VmRSS 

183 

184 return MemoryInfo(rss, vms) 

185 

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 ] 

211 

212 def GetMemoryUsage(self) -> MemoryInfo: 

213 """ 

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

215 

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) 

220 

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

222 (errno is set). PROC_PIDTASKINFO = 4. 

223 

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 

229 

230 PROC_PIDTASKINFO = 4 

231 

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 ] 

242 

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

248 

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

250 

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

254 

255 from ctypes.wintypes import DWORD 

256 

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 ] 

269 

270 del DWORD 

271 

272 def GetMemoryUsage(self) -> MemoryInfo: 

273 """ 

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

275 

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) 

279 

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. 

283 

284 :returns: Memory usage of the current process. 

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

286 """ 

287 

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

289 

290 self._psapi.GetProcessMemoryInfo.restype = BOOL 

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

292 

293 processMemoryCounters = self._ProcessMemoryCounters() 

294 processMemoryCounters.cb = sizeof(processMemoryCounters) 

295 

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

298 

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

300 

301 else: 

302 raise PlatformException(f"Unsupported platform: '{CurrentPlatform}'.")