python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
"""Process-tree memory measurement and escalation for wiki exports.
|
|
2
|
+
|
|
3
|
+
The guard is deliberately independent of the exporter: its callbacks change
|
|
4
|
+
execution strategy, while callers retain sole ownership of output payloads.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ctypes
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from collections import defaultdict, deque
|
|
14
|
+
from collections.abc import Callable, Mapping
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from time import monotonic
|
|
20
|
+
|
|
21
|
+
from .agent_cache import current_process_rss_bytes, parse_memory_size
|
|
22
|
+
|
|
23
|
+
GIB = 1024**3
|
|
24
|
+
DEFAULT_MEMORY_LIMIT_BYTES = 4 * GIB
|
|
25
|
+
WIKI_MAX_MEMORY_ENV = "DELPHI_LSP_WIKI_MAX_MEMORY"
|
|
26
|
+
_PROC_PIDTBSDINFO = 3
|
|
27
|
+
_PROC_PIDTASKINFO = 4
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class ProcessMemorySnapshot:
|
|
32
|
+
"""One process's parent relationship and resident memory usage."""
|
|
33
|
+
|
|
34
|
+
parent_pid: int
|
|
35
|
+
rss_bytes: int
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MemoryStage(str, Enum):
|
|
39
|
+
COMPACT = "compact"
|
|
40
|
+
REDUCE_WORKERS = "reduce_workers"
|
|
41
|
+
SEQUENTIAL = "sequential"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class MemoryTransition:
|
|
46
|
+
"""An escalation that callers may surface through progress reporting."""
|
|
47
|
+
|
|
48
|
+
stage: MemoryStage
|
|
49
|
+
phase: str
|
|
50
|
+
used_bytes: int
|
|
51
|
+
limit_bytes: int
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def utilization_percent(self) -> float:
|
|
55
|
+
return self.used_bytes * 100.0 / self.limit_bytes
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WikiMemoryLimitExceeded(RuntimeError):
|
|
59
|
+
"""Raised when memory remains over budget after sequentialization."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, *, phase: str, used_bytes: int, limit_bytes: int) -> None:
|
|
62
|
+
self.phase = phase
|
|
63
|
+
self.used_bytes = used_bytes
|
|
64
|
+
self.limit_bytes = limit_bytes
|
|
65
|
+
super().__init__(
|
|
66
|
+
f"Wiki export exceeded its {limit_bytes} byte memory limit during "
|
|
67
|
+
f"{phase} after switching to sequential processing ({used_bytes} bytes)."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def system_memory_bytes() -> int:
|
|
72
|
+
"""Return physical memory without optional dependencies, or zero on failure."""
|
|
73
|
+
try:
|
|
74
|
+
if os.name == "nt":
|
|
75
|
+
return _windows_system_memory_bytes()
|
|
76
|
+
if sys.platform == "darwin":
|
|
77
|
+
return _darwin_system_memory_bytes()
|
|
78
|
+
if sys.platform.startswith("linux"):
|
|
79
|
+
return _linux_system_memory_bytes()
|
|
80
|
+
except Exception: # noqa: BLE001, S110 - probes must never disrupt export.
|
|
81
|
+
pass
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _linux_system_memory_bytes() -> int:
|
|
86
|
+
for line in Path("/proc/meminfo").read_text(encoding="ascii").splitlines():
|
|
87
|
+
field, _, value = line.partition(":")
|
|
88
|
+
if field == "MemTotal":
|
|
89
|
+
return int(value.split()[0]) * 1024
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _darwin_system_memory_bytes() -> int:
|
|
94
|
+
result = subprocess.run(
|
|
95
|
+
["/usr/sbin/sysctl", "-n", "hw.memsize"],
|
|
96
|
+
check=True,
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
)
|
|
100
|
+
return int(result.stdout.strip())
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _windows_system_memory_bytes() -> int:
|
|
104
|
+
class MemoryStatusEx(ctypes.Structure):
|
|
105
|
+
_fields_ = [
|
|
106
|
+
("dwLength", ctypes.c_ulong),
|
|
107
|
+
("dwMemoryLoad", ctypes.c_ulong),
|
|
108
|
+
("ullTotalPhys", ctypes.c_ulonglong),
|
|
109
|
+
("ullAvailPhys", ctypes.c_ulonglong),
|
|
110
|
+
("ullTotalPageFile", ctypes.c_ulonglong),
|
|
111
|
+
("ullAvailPageFile", ctypes.c_ulonglong),
|
|
112
|
+
("ullTotalVirtual", ctypes.c_ulonglong),
|
|
113
|
+
("ullAvailVirtual", ctypes.c_ulonglong),
|
|
114
|
+
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
status = MemoryStatusEx()
|
|
118
|
+
status.dwLength = ctypes.sizeof(status)
|
|
119
|
+
windll = ctypes.windll # type: ignore[attr-defined]
|
|
120
|
+
if not windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)):
|
|
121
|
+
return 0
|
|
122
|
+
return int(status.ullTotalPhys)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def default_memory_limit_bytes() -> int:
|
|
126
|
+
"""Return 90% of physical memory, capped at 4 GiB, with a 4 GiB fallback."""
|
|
127
|
+
total = system_memory_bytes()
|
|
128
|
+
return min(DEFAULT_MEMORY_LIMIT_BYTES, total * 90 // 100) if total > 0 else DEFAULT_MEMORY_LIMIT_BYTES
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse_optional_memory_limit(value: str | None) -> int | None:
|
|
132
|
+
"""Parse a user override without allowing an invalid optional value to crash setup."""
|
|
133
|
+
if value is None or not value.strip():
|
|
134
|
+
return None
|
|
135
|
+
try:
|
|
136
|
+
return parse_memory_size(value)
|
|
137
|
+
except (TypeError, ValueError):
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def resolve_memory_limit_bytes(
|
|
142
|
+
explicit_limit_bytes: int | None,
|
|
143
|
+
*,
|
|
144
|
+
environ: Mapping[str, str] | None = None,
|
|
145
|
+
) -> int:
|
|
146
|
+
"""Resolve an explicit or environment override, otherwise use the safe default."""
|
|
147
|
+
if explicit_limit_bytes is not None:
|
|
148
|
+
if explicit_limit_bytes <= 0:
|
|
149
|
+
raise ValueError("wiki max memory must be greater than zero")
|
|
150
|
+
return explicit_limit_bytes
|
|
151
|
+
selected_environment = os.environ if environ is None else environ
|
|
152
|
+
raw_value = selected_environment.get(WIKI_MAX_MEMORY_ENV)
|
|
153
|
+
if raw_value is None or not raw_value.strip():
|
|
154
|
+
return default_memory_limit_bytes()
|
|
155
|
+
try:
|
|
156
|
+
value = parse_memory_size(raw_value)
|
|
157
|
+
except (TypeError, ValueError) as error:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"{WIKI_MAX_MEMORY_ENV} must be a positive memory size"
|
|
160
|
+
) from error
|
|
161
|
+
if value <= 0:
|
|
162
|
+
raise ValueError(f"{WIKI_MAX_MEMORY_ENV} must be greater than zero")
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def process_tree_rss_bytes(
|
|
167
|
+
root_pid: int | None = None,
|
|
168
|
+
*,
|
|
169
|
+
snapshot: Mapping[int, ProcessMemorySnapshot] | None = None,
|
|
170
|
+
) -> int:
|
|
171
|
+
"""Return resident memory for a process and all descendants, or zero on failure."""
|
|
172
|
+
if root_pid is None:
|
|
173
|
+
root_pid = os.getpid()
|
|
174
|
+
try:
|
|
175
|
+
if snapshot is not None:
|
|
176
|
+
processes = snapshot
|
|
177
|
+
else:
|
|
178
|
+
try:
|
|
179
|
+
processes = _process_memory_snapshot()
|
|
180
|
+
except Exception: # noqa: BLE001 - fall back to the root process below.
|
|
181
|
+
processes = {}
|
|
182
|
+
if not processes and root_pid == os.getpid():
|
|
183
|
+
current = current_process_rss_bytes()
|
|
184
|
+
if current > 0:
|
|
185
|
+
return current
|
|
186
|
+
import resource
|
|
187
|
+
|
|
188
|
+
maximum = int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
|
|
189
|
+
return maximum if sys.platform == "darwin" else maximum * 1024
|
|
190
|
+
return _sum_descendant_rss(root_pid, processes)
|
|
191
|
+
except Exception: # noqa: BLE001 - process inspection is best effort.
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _sum_descendant_rss(
|
|
196
|
+
root_pid: int, snapshot: Mapping[int, ProcessMemorySnapshot]
|
|
197
|
+
) -> int:
|
|
198
|
+
children: dict[int, list[int]] = defaultdict(list)
|
|
199
|
+
for pid, item in snapshot.items():
|
|
200
|
+
children[item.parent_pid].append(pid)
|
|
201
|
+
pending = deque([root_pid])
|
|
202
|
+
seen: set[int] = set()
|
|
203
|
+
total = 0
|
|
204
|
+
while pending:
|
|
205
|
+
pid = pending.popleft()
|
|
206
|
+
if pid in seen:
|
|
207
|
+
continue
|
|
208
|
+
seen.add(pid)
|
|
209
|
+
current = snapshot.get(pid)
|
|
210
|
+
if current is not None:
|
|
211
|
+
total += max(0, current.rss_bytes)
|
|
212
|
+
pending.extend(children.get(pid, ()))
|
|
213
|
+
return total
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _process_memory_snapshot() -> dict[int, ProcessMemorySnapshot]:
|
|
217
|
+
if os.name == "nt":
|
|
218
|
+
return _windows_process_snapshot()
|
|
219
|
+
if sys.platform.startswith("linux"):
|
|
220
|
+
return _linux_process_snapshot()
|
|
221
|
+
if sys.platform == "darwin":
|
|
222
|
+
return _darwin_process_snapshot()
|
|
223
|
+
return {}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _linux_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
|
|
227
|
+
result: dict[int, ProcessMemorySnapshot] = {}
|
|
228
|
+
for entry in Path("/proc").iterdir():
|
|
229
|
+
if not entry.name.isdecimal():
|
|
230
|
+
continue
|
|
231
|
+
try:
|
|
232
|
+
fields = entry.joinpath("status").read_text(encoding="ascii").splitlines()
|
|
233
|
+
values = {line.split(":", 1)[0]: line.split(":", 1)[1].split()[0] for line in fields if ":" in line}
|
|
234
|
+
result[int(entry.name)] = ProcessMemorySnapshot(
|
|
235
|
+
parent_pid=int(values["PPid"]), rss_bytes=int(values.get("VmRSS", 0)) * 1024
|
|
236
|
+
)
|
|
237
|
+
except (OSError, KeyError, ValueError, IndexError):
|
|
238
|
+
continue
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _darwin_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
|
|
243
|
+
try:
|
|
244
|
+
return _darwin_libproc_process_snapshot()
|
|
245
|
+
except Exception: # noqa: BLE001 - retain the portable command fallback.
|
|
246
|
+
return _darwin_ps_process_snapshot()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@lru_cache(maxsize=1)
|
|
250
|
+
def _load_darwin_libproc() -> ctypes.CDLL:
|
|
251
|
+
libproc = ctypes.CDLL("/usr/lib/libproc.dylib", use_errno=True)
|
|
252
|
+
libproc.proc_listallpids.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
253
|
+
libproc.proc_listallpids.restype = ctypes.c_int
|
|
254
|
+
libproc.proc_pidinfo.argtypes = [
|
|
255
|
+
ctypes.c_int,
|
|
256
|
+
ctypes.c_int,
|
|
257
|
+
ctypes.c_uint64,
|
|
258
|
+
ctypes.c_void_p,
|
|
259
|
+
ctypes.c_int,
|
|
260
|
+
]
|
|
261
|
+
libproc.proc_pidinfo.restype = ctypes.c_int
|
|
262
|
+
return libproc
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _darwin_libproc_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
|
|
266
|
+
"""Read the macOS process table without spawning ``ps`` at every sample."""
|
|
267
|
+
libproc = _load_darwin_libproc()
|
|
268
|
+
process_count = int(libproc.proc_listallpids(None, 0))
|
|
269
|
+
if process_count <= 0:
|
|
270
|
+
raise OSError("libproc returned no process capacity")
|
|
271
|
+
capacity = max(64, process_count + 64)
|
|
272
|
+
while True:
|
|
273
|
+
pids = (ctypes.c_int * capacity)()
|
|
274
|
+
returned = int(libproc.proc_listallpids(pids, ctypes.sizeof(pids)))
|
|
275
|
+
if returned < 0:
|
|
276
|
+
raise OSError(ctypes.get_errno(), "proc_listallpids failed")
|
|
277
|
+
if returned < capacity:
|
|
278
|
+
break
|
|
279
|
+
capacity *= 2
|
|
280
|
+
|
|
281
|
+
result: dict[int, ProcessMemorySnapshot] = {}
|
|
282
|
+
bsd_info = (ctypes.c_ubyte * 512)()
|
|
283
|
+
task_info = (ctypes.c_ubyte * 256)()
|
|
284
|
+
for raw_pid in pids[:returned]:
|
|
285
|
+
pid = int(raw_pid)
|
|
286
|
+
if pid <= 0:
|
|
287
|
+
continue
|
|
288
|
+
bsd_size = int(
|
|
289
|
+
libproc.proc_pidinfo(
|
|
290
|
+
pid,
|
|
291
|
+
_PROC_PIDTBSDINFO,
|
|
292
|
+
0,
|
|
293
|
+
bsd_info,
|
|
294
|
+
ctypes.sizeof(bsd_info),
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
if bsd_size < 20:
|
|
298
|
+
continue
|
|
299
|
+
task_size = int(
|
|
300
|
+
libproc.proc_pidinfo(
|
|
301
|
+
pid,
|
|
302
|
+
_PROC_PIDTASKINFO,
|
|
303
|
+
0,
|
|
304
|
+
task_info,
|
|
305
|
+
ctypes.sizeof(task_info),
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
if task_size < 16:
|
|
309
|
+
continue
|
|
310
|
+
parent_pid = int(ctypes.c_uint32.from_buffer(bsd_info, 16).value)
|
|
311
|
+
rss_bytes = int(ctypes.c_uint64.from_buffer(task_info, 8).value)
|
|
312
|
+
result[pid] = ProcessMemorySnapshot(parent_pid, rss_bytes)
|
|
313
|
+
if not result:
|
|
314
|
+
raise OSError("libproc returned no readable processes")
|
|
315
|
+
return result
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _darwin_ps_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
|
|
319
|
+
output = subprocess.run(
|
|
320
|
+
["/bin/ps", "-axo", "pid=,ppid=,rss="],
|
|
321
|
+
check=True,
|
|
322
|
+
capture_output=True,
|
|
323
|
+
text=True,
|
|
324
|
+
).stdout
|
|
325
|
+
result: dict[int, ProcessMemorySnapshot] = {}
|
|
326
|
+
for line in output.splitlines():
|
|
327
|
+
try:
|
|
328
|
+
pid, parent_pid, rss_kib = line.split()
|
|
329
|
+
result[int(pid)] = ProcessMemorySnapshot(int(parent_pid), int(rss_kib) * 1024)
|
|
330
|
+
except ValueError:
|
|
331
|
+
continue
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _windows_process_snapshot() -> dict[int, ProcessMemorySnapshot]:
|
|
336
|
+
"""Collect parent links and working sets with ToolHelp/PSAPI, no third-party API."""
|
|
337
|
+
from ctypes import wintypes
|
|
338
|
+
|
|
339
|
+
class ProcessEntry32(ctypes.Structure):
|
|
340
|
+
_fields_ = [
|
|
341
|
+
("dwSize", wintypes.DWORD),
|
|
342
|
+
("cntUsage", wintypes.DWORD),
|
|
343
|
+
("th32ProcessID", wintypes.DWORD),
|
|
344
|
+
("th32DefaultHeapID", ctypes.c_size_t),
|
|
345
|
+
("th32ModuleID", wintypes.DWORD),
|
|
346
|
+
("cntThreads", wintypes.DWORD),
|
|
347
|
+
("th32ParentProcessID", wintypes.DWORD),
|
|
348
|
+
("pcPriClassBase", ctypes.c_long),
|
|
349
|
+
("dwFlags", wintypes.DWORD),
|
|
350
|
+
("szExeFile", ctypes.c_wchar * 260),
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
class ProcessMemoryCounters(ctypes.Structure):
|
|
354
|
+
_fields_ = [
|
|
355
|
+
("cb", wintypes.DWORD),
|
|
356
|
+
("PageFaultCount", wintypes.DWORD),
|
|
357
|
+
("PeakWorkingSetSize", ctypes.c_size_t),
|
|
358
|
+
("WorkingSetSize", ctypes.c_size_t),
|
|
359
|
+
("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
|
|
360
|
+
("QuotaPagedPoolUsage", ctypes.c_size_t),
|
|
361
|
+
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
|
|
362
|
+
("QuotaNonPagedPoolUsage", ctypes.c_size_t),
|
|
363
|
+
("PagefileUsage", ctypes.c_size_t),
|
|
364
|
+
("PeakPagefileUsage", ctypes.c_size_t),
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
windll = ctypes.windll # type: ignore[attr-defined]
|
|
368
|
+
kernel32 = windll.kernel32
|
|
369
|
+
psapi = windll.psapi
|
|
370
|
+
kernel32.CreateToolhelp32Snapshot.argtypes = [wintypes.DWORD, wintypes.DWORD]
|
|
371
|
+
kernel32.CreateToolhelp32Snapshot.restype = wintypes.HANDLE
|
|
372
|
+
kernel32.Process32FirstW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry32)]
|
|
373
|
+
kernel32.Process32FirstW.restype = wintypes.BOOL
|
|
374
|
+
kernel32.Process32NextW.argtypes = [wintypes.HANDLE, ctypes.POINTER(ProcessEntry32)]
|
|
375
|
+
kernel32.Process32NextW.restype = wintypes.BOOL
|
|
376
|
+
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
377
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
378
|
+
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
379
|
+
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
380
|
+
psapi.GetProcessMemoryInfo.argtypes = [
|
|
381
|
+
wintypes.HANDLE,
|
|
382
|
+
ctypes.POINTER(ProcessMemoryCounters),
|
|
383
|
+
wintypes.DWORD,
|
|
384
|
+
]
|
|
385
|
+
psapi.GetProcessMemoryInfo.restype = wintypes.BOOL
|
|
386
|
+
snapshot = kernel32.CreateToolhelp32Snapshot(0x00000002, 0)
|
|
387
|
+
if not snapshot or snapshot == wintypes.HANDLE(-1).value:
|
|
388
|
+
return {}
|
|
389
|
+
result: dict[int, ProcessMemorySnapshot] = {}
|
|
390
|
+
try:
|
|
391
|
+
entry = ProcessEntry32()
|
|
392
|
+
entry.dwSize = ctypes.sizeof(entry)
|
|
393
|
+
has_process = kernel32.Process32FirstW(snapshot, ctypes.byref(entry))
|
|
394
|
+
while has_process:
|
|
395
|
+
process = kernel32.OpenProcess(0x0400 | 0x0010, False, entry.th32ProcessID)
|
|
396
|
+
rss_bytes = 0
|
|
397
|
+
if process:
|
|
398
|
+
try:
|
|
399
|
+
counters = ProcessMemoryCounters()
|
|
400
|
+
counters.cb = ctypes.sizeof(counters)
|
|
401
|
+
if psapi.GetProcessMemoryInfo(process, ctypes.byref(counters), counters.cb):
|
|
402
|
+
rss_bytes = int(counters.WorkingSetSize)
|
|
403
|
+
finally:
|
|
404
|
+
kernel32.CloseHandle(process)
|
|
405
|
+
result[int(entry.th32ProcessID)] = ProcessMemorySnapshot(
|
|
406
|
+
int(entry.th32ParentProcessID), rss_bytes
|
|
407
|
+
)
|
|
408
|
+
entry.dwSize = ctypes.sizeof(entry)
|
|
409
|
+
has_process = kernel32.Process32NextW(snapshot, ctypes.byref(entry))
|
|
410
|
+
finally:
|
|
411
|
+
kernel32.CloseHandle(snapshot)
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class ProcessTreeMemorySampler:
|
|
416
|
+
"""Measure root RSS cheaply while periodically refreshing child RSS."""
|
|
417
|
+
|
|
418
|
+
def __init__(
|
|
419
|
+
self,
|
|
420
|
+
*,
|
|
421
|
+
interval_seconds: float = 5.0,
|
|
422
|
+
root_measure: Callable[[], int] = current_process_rss_bytes,
|
|
423
|
+
tree_measure: Callable[[], int] = process_tree_rss_bytes,
|
|
424
|
+
clock: Callable[[], float] = monotonic,
|
|
425
|
+
) -> None:
|
|
426
|
+
if interval_seconds <= 0:
|
|
427
|
+
raise ValueError("interval_seconds must be greater than zero")
|
|
428
|
+
self.interval_seconds = interval_seconds
|
|
429
|
+
self._root_measure = root_measure
|
|
430
|
+
self._tree_measure = tree_measure
|
|
431
|
+
self._clock = clock
|
|
432
|
+
self._last_tree_sample = float("-inf")
|
|
433
|
+
self._cached_child_bytes = 0
|
|
434
|
+
|
|
435
|
+
def __call__(self) -> int:
|
|
436
|
+
root_bytes = self._root_measure()
|
|
437
|
+
now = self._clock()
|
|
438
|
+
if now - self._last_tree_sample >= self.interval_seconds:
|
|
439
|
+
return self._sample_tree(root_bytes, now)
|
|
440
|
+
return max(0, root_bytes) + self._cached_child_bytes
|
|
441
|
+
|
|
442
|
+
def force(self) -> int:
|
|
443
|
+
"""Refresh the full process tree regardless of the sampling interval."""
|
|
444
|
+
root_bytes = self._root_measure()
|
|
445
|
+
return self._sample_tree(root_bytes, self._clock())
|
|
446
|
+
|
|
447
|
+
def _sample_tree(self, root_bytes: int, now: float) -> int:
|
|
448
|
+
tree_bytes = self._tree_measure()
|
|
449
|
+
self._last_tree_sample = now
|
|
450
|
+
if tree_bytes <= 0:
|
|
451
|
+
self._cached_child_bytes = 0
|
|
452
|
+
return max(0, root_bytes)
|
|
453
|
+
self._cached_child_bytes = max(0, tree_bytes - max(0, root_bytes))
|
|
454
|
+
return max(0, root_bytes) + self._cached_child_bytes
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class WikiMemoryGuard:
|
|
458
|
+
"""Escalate wiki-export execution strategy without touching emitted data."""
|
|
459
|
+
|
|
460
|
+
def __init__(
|
|
461
|
+
self,
|
|
462
|
+
*,
|
|
463
|
+
limit_bytes: int | None = None,
|
|
464
|
+
measure: Callable[[], int] | None = None,
|
|
465
|
+
compact: Callable[[], None] | None = None,
|
|
466
|
+
reduce_workers: Callable[[], None] | None = None,
|
|
467
|
+
sequentialize: Callable[[], None] | None = None,
|
|
468
|
+
on_transition: Callable[[MemoryTransition], None] | None = None,
|
|
469
|
+
) -> None:
|
|
470
|
+
self.limit_bytes = limit_bytes if limit_bytes is not None else default_memory_limit_bytes()
|
|
471
|
+
if self.limit_bytes <= 0:
|
|
472
|
+
raise ValueError("limit_bytes must be greater than zero")
|
|
473
|
+
self._measure = measure or ProcessTreeMemorySampler()
|
|
474
|
+
self._actions = {
|
|
475
|
+
MemoryStage.COMPACT: compact or (lambda: None),
|
|
476
|
+
MemoryStage.REDUCE_WORKERS: reduce_workers or (lambda: None),
|
|
477
|
+
MemoryStage.SEQUENTIAL: sequentialize or (lambda: None),
|
|
478
|
+
}
|
|
479
|
+
self._on_transition = on_transition
|
|
480
|
+
self._completed: set[MemoryStage] = set()
|
|
481
|
+
self.peak_bytes = 0
|
|
482
|
+
self.phase_peaks: dict[str, int] = {}
|
|
483
|
+
|
|
484
|
+
def check(
|
|
485
|
+
self,
|
|
486
|
+
used_bytes: int | None = None,
|
|
487
|
+
*,
|
|
488
|
+
phase: str,
|
|
489
|
+
force_process_tree: bool = False,
|
|
490
|
+
) -> MemoryTransition | None:
|
|
491
|
+
"""Apply at most one new stage and return it for progress reporting."""
|
|
492
|
+
if used_bytes is not None:
|
|
493
|
+
used = used_bytes
|
|
494
|
+
elif force_process_tree and isinstance(self._measure, ProcessTreeMemorySampler):
|
|
495
|
+
used = self._measure.force()
|
|
496
|
+
else:
|
|
497
|
+
used = self._measure()
|
|
498
|
+
self.peak_bytes = max(self.peak_bytes, used)
|
|
499
|
+
self.phase_peaks[phase] = max(self.phase_peaks.get(phase, 0), used)
|
|
500
|
+
if used <= 0:
|
|
501
|
+
return None
|
|
502
|
+
required_stage = self._required_stage(used)
|
|
503
|
+
if required_stage is None:
|
|
504
|
+
return None
|
|
505
|
+
ordered_stages = (
|
|
506
|
+
MemoryStage.COMPACT,
|
|
507
|
+
MemoryStage.REDUCE_WORKERS,
|
|
508
|
+
MemoryStage.SEQUENTIAL,
|
|
509
|
+
)
|
|
510
|
+
required_index = ordered_stages.index(required_stage)
|
|
511
|
+
if required_stage in self._completed:
|
|
512
|
+
if required_stage is MemoryStage.SEQUENTIAL:
|
|
513
|
+
if (
|
|
514
|
+
used_bytes is None
|
|
515
|
+
and not force_process_tree
|
|
516
|
+
and isinstance(self._measure, ProcessTreeMemorySampler)
|
|
517
|
+
):
|
|
518
|
+
used = self._measure.force()
|
|
519
|
+
self.peak_bytes = max(self.peak_bytes, used)
|
|
520
|
+
self.phase_peaks[phase] = max(
|
|
521
|
+
self.phase_peaks.get(phase, 0),
|
|
522
|
+
used,
|
|
523
|
+
)
|
|
524
|
+
if self._required_stage(used) is not MemoryStage.SEQUENTIAL:
|
|
525
|
+
return None
|
|
526
|
+
raise WikiMemoryLimitExceeded(phase=phase, used_bytes=used, limit_bytes=self.limit_bytes)
|
|
527
|
+
return None
|
|
528
|
+
latest_transition: MemoryTransition | None = None
|
|
529
|
+
for stage in ordered_stages[: required_index + 1]:
|
|
530
|
+
if stage in self._completed:
|
|
531
|
+
continue
|
|
532
|
+
transition = MemoryTransition(stage, phase, used, self.limit_bytes)
|
|
533
|
+
self._actions[stage]()
|
|
534
|
+
self._completed.add(stage)
|
|
535
|
+
if self._on_transition is not None:
|
|
536
|
+
self._on_transition(transition)
|
|
537
|
+
latest_transition = transition
|
|
538
|
+
return latest_transition
|
|
539
|
+
|
|
540
|
+
def _required_stage(self, used_bytes: int) -> MemoryStage | None:
|
|
541
|
+
utilization = used_bytes * 100 / self.limit_bytes
|
|
542
|
+
if utilization >= 90:
|
|
543
|
+
return MemoryStage.SEQUENTIAL
|
|
544
|
+
if utilization >= 80:
|
|
545
|
+
return MemoryStage.REDUCE_WORKERS
|
|
546
|
+
if utilization >= 60:
|
|
547
|
+
return MemoryStage.COMPACT
|
|
548
|
+
return None
|
delphi_lsp/workspace.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from .consts import AttributeName
|
|
7
|
+
from .nodes import SyntaxNode
|
|
8
|
+
from .parser import DelphiParser
|
|
9
|
+
from .preprocessor import IncludeLoader, PreprocessedSource
|
|
10
|
+
from .semantic import Scope, ScopeKind, SymbolIndex
|
|
11
|
+
from .semantic_builder import SemanticBuilder, SemanticModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class WorkspaceSemanticResult:
|
|
16
|
+
models: dict[str, SemanticModel]
|
|
17
|
+
index: SymbolIndex
|
|
18
|
+
preprocessed: dict[str, PreprocessedSource] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_workspace_semantics(
|
|
22
|
+
sources: dict[str, str],
|
|
23
|
+
*,
|
|
24
|
+
include_paths: Iterable[str] = (),
|
|
25
|
+
defines: Iterable[str] = (),
|
|
26
|
+
include_loader: IncludeLoader | None = None,
|
|
27
|
+
preprocessor_options=None,
|
|
28
|
+
collect_references: bool = True,
|
|
29
|
+
) -> WorkspaceSemanticResult:
|
|
30
|
+
parser = DelphiParser(
|
|
31
|
+
include_paths=include_paths,
|
|
32
|
+
defines=defines,
|
|
33
|
+
include_loader=include_loader,
|
|
34
|
+
preprocessor_options=preprocessor_options,
|
|
35
|
+
)
|
|
36
|
+
roots: dict[str, SyntaxNode] = {}
|
|
37
|
+
preprocessed_sources: dict[str, PreprocessedSource] = {}
|
|
38
|
+
for file_name, text in sources.items():
|
|
39
|
+
result = parser.parse(text, file_name, build_semantic=False)
|
|
40
|
+
roots[file_name] = result.root
|
|
41
|
+
preprocessed_sources[file_name] = result.preprocessed
|
|
42
|
+
|
|
43
|
+
return build_workspace_semantics_from_roots(
|
|
44
|
+
roots,
|
|
45
|
+
preprocessed_sources=preprocessed_sources,
|
|
46
|
+
collect_references=collect_references,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_workspace_semantics_from_roots(
|
|
51
|
+
roots: dict[str, SyntaxNode],
|
|
52
|
+
*,
|
|
53
|
+
preprocessed_sources: Mapping[str, PreprocessedSource] | None = None,
|
|
54
|
+
collect_references: bool = True,
|
|
55
|
+
) -> WorkspaceSemanticResult:
|
|
56
|
+
"""Build project semantics from syntax roots that have already been parsed."""
|
|
57
|
+
|
|
58
|
+
index = SymbolIndex()
|
|
59
|
+
scopes: dict[str, Scope] = {}
|
|
60
|
+
for file_name, root in roots.items():
|
|
61
|
+
unit_name = root.get_attribute(AttributeName.anName) or file_name
|
|
62
|
+
unit_scope = Scope(kind=ScopeKind.UNIT, name=unit_name)
|
|
63
|
+
index.register_unit(unit_name, unit_scope, index_symbols=False)
|
|
64
|
+
scopes[file_name] = unit_scope
|
|
65
|
+
|
|
66
|
+
source_maps = preprocessed_sources or {}
|
|
67
|
+
builder = SemanticBuilder(
|
|
68
|
+
collect_references=collect_references,
|
|
69
|
+
source_maps=source_maps,
|
|
70
|
+
)
|
|
71
|
+
for file_name, root in roots.items():
|
|
72
|
+
builder.declare(root, index=index, unit_scope=scopes[file_name], reset_state=False)
|
|
73
|
+
|
|
74
|
+
models: dict[str, SemanticModel] = {}
|
|
75
|
+
for file_name, root in roots.items():
|
|
76
|
+
model = builder.resolve(root, scopes[file_name])
|
|
77
|
+
models[file_name] = model
|
|
78
|
+
|
|
79
|
+
return WorkspaceSemanticResult(
|
|
80
|
+
models=models,
|
|
81
|
+
index=index,
|
|
82
|
+
preprocessed=dict(source_maps),
|
|
83
|
+
)
|
delphi_lsp/writer.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .consts import ATTRIBUTE_NAME_STRINGS, SYNTAX_NODE_NAMES
|
|
4
|
+
from .nodes import CompoundSyntaxNode, SyntaxNode, ValuedSyntaxNode
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _xml_encode(data: str) -> str:
|
|
8
|
+
result = []
|
|
9
|
+
for ch in data:
|
|
10
|
+
if ch == '<':
|
|
11
|
+
result.append('<')
|
|
12
|
+
elif ch == '>':
|
|
13
|
+
result.append('>')
|
|
14
|
+
elif ch == '&':
|
|
15
|
+
result.append('&')
|
|
16
|
+
elif ch == '"':
|
|
17
|
+
result.append('"')
|
|
18
|
+
elif ch == "'":
|
|
19
|
+
result.append(''')
|
|
20
|
+
else:
|
|
21
|
+
result.append(ch)
|
|
22
|
+
return ''.join(result)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SyntaxTreeWriter:
|
|
26
|
+
@staticmethod
|
|
27
|
+
def to_xml(root: SyntaxNode, formatted: bool = False) -> str:
|
|
28
|
+
builder: list[str] = []
|
|
29
|
+
SyntaxTreeWriter._node_to_xml(builder, root, formatted, '')
|
|
30
|
+
return '<?xml version="1.0"?>\n' + ''.join(builder)
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _node_to_xml(builder: list[str], node: SyntaxNode, formatted: bool, indent: str) -> None:
|
|
34
|
+
has_children = node.has_children
|
|
35
|
+
new_indent = indent + ' '
|
|
36
|
+
if formatted:
|
|
37
|
+
builder.append(indent)
|
|
38
|
+
builder.append('<' + SYNTAX_NODE_NAMES[node.typ].upper())
|
|
39
|
+
|
|
40
|
+
if isinstance(node, CompoundSyntaxNode):
|
|
41
|
+
builder.append(f' begin_line="{node.line}"')
|
|
42
|
+
builder.append(f' begin_col="{node.col}"')
|
|
43
|
+
builder.append(f' end_line="{node.end_line}"')
|
|
44
|
+
builder.append(f' end_col="{node.end_col}"')
|
|
45
|
+
else:
|
|
46
|
+
builder.append(f' line="{node.line}"')
|
|
47
|
+
builder.append(f' col="{node.col}"')
|
|
48
|
+
|
|
49
|
+
if node.file_name:
|
|
50
|
+
builder.append(' file="' + _xml_encode(node.file_name) + '"')
|
|
51
|
+
|
|
52
|
+
if isinstance(node, ValuedSyntaxNode):
|
|
53
|
+
builder.append(' value="' + _xml_encode(node.value) + '"')
|
|
54
|
+
|
|
55
|
+
for attr_key, attr_value in node.attributes:
|
|
56
|
+
builder.append(
|
|
57
|
+
' ' + ATTRIBUTE_NAME_STRINGS[attr_key] + '="' + _xml_encode(attr_value) + '"'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if has_children:
|
|
61
|
+
builder.append('>')
|
|
62
|
+
else:
|
|
63
|
+
builder.append('/>')
|
|
64
|
+
if formatted:
|
|
65
|
+
builder.append('\n')
|
|
66
|
+
for child in node.child_nodes:
|
|
67
|
+
SyntaxTreeWriter._node_to_xml(builder, child, formatted, new_indent)
|
|
68
|
+
if has_children:
|
|
69
|
+
if formatted:
|
|
70
|
+
builder.append(indent)
|
|
71
|
+
builder.append('</' + SYNTAX_NODE_NAMES[node.typ].upper() + '>')
|
|
72
|
+
if formatted:
|
|
73
|
+
builder.append('\n')
|