Coverage for klayout_pex/util/peak_memory.py: 64%
47 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2026 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
25"""
26The peak memory of a run as resident set size, to size the next run on a larger layout (#409):
27of this process, and of its largest subprocess that has ended (e.g. KLayout's LVS, FasterCap)
28"""
30from __future__ import annotations
32from dataclasses import dataclass
33import sys
34from typing import *
36from ..log import info
39@dataclass(frozen=True)
40class PeakMemory:
41 process: int # bytes
43 # bytes, 0 before one has ended, None where the platform doesn't tell (Windows),
44 # NOTE: on Linux at least the resident set size of this process when it started it, which Linux carries over
45 largest_subprocess: Optional[int]
48def _windows_peak_working_set() -> int:
49 """
50 :return: the peak working set of this process in bytes, the resident set size of Windows
51 """
52 import ctypes
53 from ctypes import wintypes
55 class ProcessMemoryCounters(ctypes.Structure):
56 _fields_ = [('cb', wintypes.DWORD),
57 ('PageFaultCount', wintypes.DWORD),
58 ('PeakWorkingSetSize', ctypes.c_size_t),
59 ('WorkingSetSize', ctypes.c_size_t),
60 ('QuotaPeakPagedPoolUsage', ctypes.c_size_t),
61 ('QuotaPagedPoolUsage', ctypes.c_size_t),
62 ('QuotaPeakNonPagedPoolUsage', ctypes.c_size_t),
63 ('QuotaNonPagedPoolUsage', ctypes.c_size_t),
64 ('PagefileUsage', ctypes.c_size_t),
65 ('PeakPagefileUsage', ctypes.c_size_t)]
67 # NOTE: a kernel32 of its own, as the types set on the functions of ctypes.windll.kernel32 would apply to all users
68 kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
69 kernel32.GetCurrentProcess.restype = wintypes.HANDLE
70 get_process_memory_info = kernel32.K32GetProcessMemoryInfo
71 get_process_memory_info.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessMemoryCounters), wintypes.DWORD]
72 get_process_memory_info.restype = wintypes.BOOL
73 counters = ProcessMemoryCounters()
74 counters.cb = ctypes.sizeof(counters)
75 if not get_process_memory_info(kernel32.GetCurrentProcess(), ctypes.byref(counters), counters.cb):
76 raise ctypes.WinError(ctypes.get_last_error())
77 return counters.PeakWorkingSetSize
80def _linux_peak_resident_set_size() -> Optional[int]:
81 """
82 :return: the peak resident set size of this process in bytes (VmHWM), None without /proc
84 NOTE: not ru_maxrss, which Linux carries over from what the process ran before exec,
85 e.g. from the large process that started it (a test runner of 1.4 GiB)
86 """
87 try:
88 with open('/proc/self/status', encoding='utf-8') as f:
89 sizes = [int(line.split()[1]) * 1024 for line in f if line.startswith('VmHWM:')] # NOTE: in kB
90 except OSError:
91 return None
92 return sizes[0] if sizes else None
95def peak_memory() -> PeakMemory:
96 """
97 :return: the peak resident set sizes so far
98 """
99 if sys.platform == 'win32':
100 return PeakMemory(process=_windows_peak_working_set(), largest_subprocess=None)
101 import resource
102 unit = 1 if sys.platform == 'darwin' else 1024 # NOTE: ru_maxrss is in bytes on macOS, in KiB on Linux
103 process = _linux_peak_resident_set_size() if sys.platform.startswith('linux') else None
104 return PeakMemory(process=process or resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * unit,
105 largest_subprocess=resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss * unit)
108def format_bytes(size: int) -> str:
109 """
110 :return: e.g. '512 B', '245.3 MiB', '5.7 GiB'
111 """
112 for unit, scale in (('GiB', 1 << 30), ('MiB', 1 << 20), ('KiB', 1 << 10)):
113 if size >= scale:
114 return f"{size / scale:.1f} {unit}"
115 return f"{size} B"
118def log_peak_memory(stage: str):
119 """
120 Logs the peak memory so far, e.g. 'Peak memory (resident set size) after FasterCap: 245.3 MiB,
121 largest subprocess 5.7 GiB', the subprocess only if it is the larger one, as on Linux a smaller one
122 may be the size of this process when it started the subprocess (see PeakMemory)
124 :param stage: of the run, e.g. 'FasterCap'
125 """
126 memory = peak_memory()
127 subprocess = f", largest subprocess {format_bytes(memory.largest_subprocess)}" \
128 if memory.largest_subprocess and memory.largest_subprocess > memory.process else ''
129 info(f"Peak memory (resident set size) after {stage}: {format_bytes(memory.process)}{subprocess}",
130 stacklevel=3) # NOTE: logged as of the caller, the stage