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,662 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import multiprocessing
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from concurrent.futures import (
|
|
8
|
+
FIRST_COMPLETED,
|
|
9
|
+
Future,
|
|
10
|
+
ProcessPoolExecutor,
|
|
11
|
+
as_completed,
|
|
12
|
+
wait,
|
|
13
|
+
)
|
|
14
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from time import monotonic, perf_counter
|
|
18
|
+
|
|
19
|
+
from .lsp_server import build_outline_semantic_model
|
|
20
|
+
from .parse_artifacts import (
|
|
21
|
+
OUTLINE_PRODUCER_VERSION,
|
|
22
|
+
ArtifactKey,
|
|
23
|
+
ArtifactStore,
|
|
24
|
+
CachedOutlineArtifact,
|
|
25
|
+
artifact_cache_disabled,
|
|
26
|
+
artifact_key,
|
|
27
|
+
load_outline_artifact,
|
|
28
|
+
store_outline_artifact,
|
|
29
|
+
)
|
|
30
|
+
from .project_config import ProjectPathConfig, workspace_include_loader
|
|
31
|
+
from .semantic_builder import SemanticModel
|
|
32
|
+
from .source_reader import read_source_text
|
|
33
|
+
|
|
34
|
+
_MEBIBYTE = 1024 * 1024
|
|
35
|
+
_WORKER_MEMORY_BYTES = 64 * _MEBIBYTE
|
|
36
|
+
_AUTO_WORKER_LIMIT = 8
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ParallelOutlineError(RuntimeError):
|
|
40
|
+
"""Raised when a parallel outline operation cannot complete."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class OutlineTask:
|
|
45
|
+
ordinal: int
|
|
46
|
+
source_path: str
|
|
47
|
+
defines: tuple[str, ...]
|
|
48
|
+
return_text: bool
|
|
49
|
+
workspace_root: str = ""
|
|
50
|
+
include_paths: tuple[str, ...] = ()
|
|
51
|
+
project_config: ProjectPathConfig | None = None
|
|
52
|
+
producer_version: str = OUTLINE_PRODUCER_VERSION
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class _OutlineWorkItem:
|
|
57
|
+
ordinal: int
|
|
58
|
+
source_path: str
|
|
59
|
+
return_text: bool
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class _OutlineWorkerContext:
|
|
64
|
+
defines: tuple[str, ...]
|
|
65
|
+
include_paths: tuple[str, ...]
|
|
66
|
+
project_config: ProjectPathConfig | None
|
|
67
|
+
include_files: tuple[str, ...] = ()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_outline_worker_context: _OutlineWorkerContext | None = None
|
|
71
|
+
_outline_worker_include_loader: Callable[[str, str], tuple[str, str] | None] | None = (
|
|
72
|
+
None
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class OutlineResult:
|
|
78
|
+
ordinal: int
|
|
79
|
+
source_path: str
|
|
80
|
+
text: str
|
|
81
|
+
model: SemanticModel | None
|
|
82
|
+
lines_processed: int
|
|
83
|
+
symbols_discovered: int
|
|
84
|
+
read_error: str = ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class ParallelBuildStats:
|
|
89
|
+
configured_workers: int
|
|
90
|
+
effective_workers: int
|
|
91
|
+
files_completed: int
|
|
92
|
+
elapsed_seconds: float
|
|
93
|
+
fallbacks: int
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class OutlineBatch:
|
|
98
|
+
results: tuple[OutlineResult, ...]
|
|
99
|
+
stats: ParallelBuildStats
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class _PreparedArtifact:
|
|
104
|
+
store: ArtifactStore
|
|
105
|
+
key: ArtifactKey
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_worker_setting(value: str) -> int:
|
|
109
|
+
if value == "auto":
|
|
110
|
+
return 0
|
|
111
|
+
try:
|
|
112
|
+
workers = int(value)
|
|
113
|
+
except ValueError as error:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"workers must be 'auto' or an integer from 1 through 32"
|
|
116
|
+
) from error
|
|
117
|
+
if not 1 <= workers <= 32:
|
|
118
|
+
raise ValueError("workers must be 'auto' or an integer from 1 through 32")
|
|
119
|
+
return workers
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_worker_count(
|
|
123
|
+
configured_workers: int,
|
|
124
|
+
*,
|
|
125
|
+
task_count: int,
|
|
126
|
+
cpu_count: int | None = None,
|
|
127
|
+
memory_budget_bytes: int | None = None,
|
|
128
|
+
) -> int:
|
|
129
|
+
if not 0 <= configured_workers <= 32:
|
|
130
|
+
raise ValueError("workers must be 'auto' or an integer from 1 through 32")
|
|
131
|
+
if task_count <= 0:
|
|
132
|
+
return 0
|
|
133
|
+
if configured_workers:
|
|
134
|
+
return min(configured_workers, task_count)
|
|
135
|
+
|
|
136
|
+
detected_cpus = os.cpu_count() if cpu_count is None else cpu_count
|
|
137
|
+
candidates = [task_count, _AUTO_WORKER_LIMIT, max(1, (detected_cpus or 1) - 1)]
|
|
138
|
+
if memory_budget_bytes is not None:
|
|
139
|
+
candidates.append(max(1, memory_budget_bytes // _WORKER_MEMORY_BYTES))
|
|
140
|
+
return min(candidates)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _parse_outline_source(
|
|
144
|
+
ordinal: int,
|
|
145
|
+
source_path: str,
|
|
146
|
+
return_text: bool,
|
|
147
|
+
*,
|
|
148
|
+
defines: tuple[str, ...],
|
|
149
|
+
include_paths: tuple[str, ...],
|
|
150
|
+
include_loader: Callable[[str, str], tuple[str, str] | None] | None,
|
|
151
|
+
) -> OutlineResult:
|
|
152
|
+
try:
|
|
153
|
+
text = read_source_text(Path(source_path))
|
|
154
|
+
except (OSError, UnicodeError) as error:
|
|
155
|
+
return OutlineResult(
|
|
156
|
+
ordinal=ordinal,
|
|
157
|
+
source_path=source_path,
|
|
158
|
+
text="",
|
|
159
|
+
model=None,
|
|
160
|
+
lines_processed=0,
|
|
161
|
+
symbols_discovered=0,
|
|
162
|
+
read_error=str(error),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
model = build_outline_semantic_model(
|
|
167
|
+
text,
|
|
168
|
+
source_path,
|
|
169
|
+
defines=defines,
|
|
170
|
+
include_paths=include_paths,
|
|
171
|
+
include_loader=include_loader,
|
|
172
|
+
)
|
|
173
|
+
except Exception as error:
|
|
174
|
+
raise ParallelOutlineError(f"failed to parse {source_path}: {error}") from error
|
|
175
|
+
return OutlineResult(
|
|
176
|
+
ordinal=ordinal,
|
|
177
|
+
source_path=source_path,
|
|
178
|
+
text=text if return_text else "",
|
|
179
|
+
model=model,
|
|
180
|
+
lines_processed=text.count("\n")
|
|
181
|
+
+ (0 if not text or text.endswith(("\n", "\r")) else 1),
|
|
182
|
+
symbols_discovered=sum(len(items) for items in model.index.name_index.values()),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _parse_outline_task(task: OutlineTask) -> OutlineResult:
|
|
187
|
+
return _parse_outline_source(
|
|
188
|
+
task.ordinal,
|
|
189
|
+
task.source_path,
|
|
190
|
+
task.return_text,
|
|
191
|
+
defines=task.defines,
|
|
192
|
+
include_paths=task.include_paths,
|
|
193
|
+
include_loader=workspace_include_loader(
|
|
194
|
+
task.project_config,
|
|
195
|
+
task.include_paths,
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _initialize_outline_worker(context: _OutlineWorkerContext) -> None:
|
|
201
|
+
global _outline_worker_context, _outline_worker_include_loader
|
|
202
|
+
_outline_worker_context = context
|
|
203
|
+
_outline_worker_include_loader = _inventory_include_loader(context)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _inventory_include_loader(
|
|
207
|
+
context: _OutlineWorkerContext,
|
|
208
|
+
) -> Callable[[str, str], tuple[str, str] | None] | None:
|
|
209
|
+
if not context.include_files:
|
|
210
|
+
return workspace_include_loader(
|
|
211
|
+
context.project_config,
|
|
212
|
+
context.include_paths,
|
|
213
|
+
)
|
|
214
|
+
known_files = {
|
|
215
|
+
os.path.normcase(os.path.normpath(path)): path for path in context.include_files
|
|
216
|
+
}
|
|
217
|
+
search_paths = tuple(os.path.normpath(path) for path in context.include_paths)
|
|
218
|
+
|
|
219
|
+
def load(parent_file: str, include_name: str) -> tuple[str, str] | None:
|
|
220
|
+
normalized_name = include_name.replace("\\", os.sep).replace("/", os.sep)
|
|
221
|
+
parent = os.path.dirname(os.path.normpath(parent_file))
|
|
222
|
+
for base in (parent, *search_paths):
|
|
223
|
+
candidate = os.path.normcase(
|
|
224
|
+
os.path.normpath(os.path.join(base, normalized_name))
|
|
225
|
+
)
|
|
226
|
+
selected = known_files.get(candidate)
|
|
227
|
+
if selected is None:
|
|
228
|
+
continue
|
|
229
|
+
selected_path = Path(selected)
|
|
230
|
+
if (
|
|
231
|
+
context.project_config is not None
|
|
232
|
+
and context.project_config.excludes_workspace_path(selected_path)
|
|
233
|
+
):
|
|
234
|
+
continue
|
|
235
|
+
try:
|
|
236
|
+
return read_source_text(selected_path), str(selected_path)
|
|
237
|
+
except (OSError, UnicodeError):
|
|
238
|
+
continue
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
return load
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _parse_outline_work_item(item: _OutlineWorkItem) -> OutlineResult:
|
|
245
|
+
context = _outline_worker_context
|
|
246
|
+
if context is None:
|
|
247
|
+
raise ParallelOutlineError("outline worker context was not initialized")
|
|
248
|
+
return _parse_outline_source(
|
|
249
|
+
item.ordinal,
|
|
250
|
+
item.source_path,
|
|
251
|
+
item.return_text,
|
|
252
|
+
defines=context.defines,
|
|
253
|
+
include_paths=context.include_paths,
|
|
254
|
+
include_loader=_outline_worker_include_loader,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _shared_worker_context(
|
|
259
|
+
tasks: tuple[OutlineTask, ...],
|
|
260
|
+
*,
|
|
261
|
+
include_files: tuple[str, ...],
|
|
262
|
+
) -> _OutlineWorkerContext | None:
|
|
263
|
+
if not tasks:
|
|
264
|
+
return None
|
|
265
|
+
first = tasks[0]
|
|
266
|
+
context = _OutlineWorkerContext(
|
|
267
|
+
first.defines,
|
|
268
|
+
first.include_paths,
|
|
269
|
+
first.project_config,
|
|
270
|
+
include_files,
|
|
271
|
+
)
|
|
272
|
+
if all(
|
|
273
|
+
task.defines == context.defines
|
|
274
|
+
and task.include_paths == context.include_paths
|
|
275
|
+
and task.project_config == context.project_config
|
|
276
|
+
for task in tasks[1:]
|
|
277
|
+
):
|
|
278
|
+
return context
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def run_outline_tasks(
|
|
283
|
+
tasks: Iterable[OutlineTask],
|
|
284
|
+
*,
|
|
285
|
+
configured_workers: int,
|
|
286
|
+
use_artifact_cache: bool = True,
|
|
287
|
+
memory_budget_bytes: int | None = None,
|
|
288
|
+
cpu_count: int | None = None,
|
|
289
|
+
on_complete: Callable[[OutlineResult], None] | None = None,
|
|
290
|
+
retain_results: bool = True,
|
|
291
|
+
task_runner: Callable[[OutlineTask], OutlineResult] | None = None,
|
|
292
|
+
include_files: Iterable[str | Path] = (),
|
|
293
|
+
heartbeat: Callable[[], object] | None = None,
|
|
294
|
+
force_heartbeat: Callable[[], object] | None = None,
|
|
295
|
+
active_worker_limit: Callable[[], int] | None = None,
|
|
296
|
+
) -> OutlineBatch:
|
|
297
|
+
task_list = tuple(tasks)
|
|
298
|
+
selected_include_files = tuple(str(path) for path in include_files)
|
|
299
|
+
operation = _parse_outline_task if task_runner is None else task_runner
|
|
300
|
+
started = perf_counter()
|
|
301
|
+
effective_workers = resolve_worker_count(
|
|
302
|
+
configured_workers,
|
|
303
|
+
task_count=len(task_list),
|
|
304
|
+
cpu_count=cpu_count,
|
|
305
|
+
memory_budget_bytes=memory_budget_bytes,
|
|
306
|
+
)
|
|
307
|
+
if task_runner is None and use_artifact_cache and not artifact_cache_disabled():
|
|
308
|
+
cached_results, pending_tasks, prepared = _prepare_artifacts(task_list)
|
|
309
|
+
else:
|
|
310
|
+
cached_results, pending_tasks, prepared = [], task_list, {}
|
|
311
|
+
accepted: list[OutlineResult] = []
|
|
312
|
+
cached_completed = len(cached_results)
|
|
313
|
+
for result in cached_results:
|
|
314
|
+
if on_complete is not None:
|
|
315
|
+
on_complete(result)
|
|
316
|
+
if retain_results:
|
|
317
|
+
accepted.append(result)
|
|
318
|
+
|
|
319
|
+
if not pending_tasks:
|
|
320
|
+
return _batch(
|
|
321
|
+
configured_workers,
|
|
322
|
+
effective_workers,
|
|
323
|
+
accepted,
|
|
324
|
+
started,
|
|
325
|
+
files_completed=cached_completed,
|
|
326
|
+
fallbacks=0,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def complete(result: OutlineResult) -> None:
|
|
330
|
+
artifact = prepared.get((result.ordinal, result.source_path))
|
|
331
|
+
if artifact is not None and not result.read_error and result.model is not None:
|
|
332
|
+
with contextlib.suppress(Exception):
|
|
333
|
+
store_outline_artifact(
|
|
334
|
+
artifact.store,
|
|
335
|
+
artifact.key,
|
|
336
|
+
result.source_path,
|
|
337
|
+
result.model,
|
|
338
|
+
lines_processed=result.lines_processed,
|
|
339
|
+
symbols_discovered=result.symbols_discovered,
|
|
340
|
+
)
|
|
341
|
+
if on_complete is not None:
|
|
342
|
+
on_complete(result)
|
|
343
|
+
|
|
344
|
+
dispatch_workers = min(effective_workers, len(pending_tasks))
|
|
345
|
+
if dispatch_workers <= 1:
|
|
346
|
+
results, serial_completed = _run_serial(
|
|
347
|
+
pending_tasks,
|
|
348
|
+
complete,
|
|
349
|
+
retain_results,
|
|
350
|
+
operation,
|
|
351
|
+
)
|
|
352
|
+
accepted.extend(results)
|
|
353
|
+
_prune_artifacts(prepared)
|
|
354
|
+
return _batch(
|
|
355
|
+
configured_workers,
|
|
356
|
+
effective_workers,
|
|
357
|
+
accepted,
|
|
358
|
+
started,
|
|
359
|
+
files_completed=cached_completed + serial_completed,
|
|
360
|
+
fallbacks=0,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
worker_context = (
|
|
364
|
+
_shared_worker_context(
|
|
365
|
+
pending_tasks,
|
|
366
|
+
include_files=selected_include_files,
|
|
367
|
+
)
|
|
368
|
+
if task_runner is None
|
|
369
|
+
else None
|
|
370
|
+
)
|
|
371
|
+
futures: dict[Future[OutlineResult], OutlineTask] = {}
|
|
372
|
+
executor: ProcessPoolExecutor | None = None
|
|
373
|
+
parallel_completed = 0
|
|
374
|
+
try:
|
|
375
|
+
context = multiprocessing.get_context("spawn")
|
|
376
|
+
if worker_context is None:
|
|
377
|
+
active_executor = ProcessPoolExecutor(
|
|
378
|
+
max_workers=dispatch_workers,
|
|
379
|
+
mp_context=context,
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
active_executor = ProcessPoolExecutor(
|
|
383
|
+
max_workers=dispatch_workers,
|
|
384
|
+
mp_context=context,
|
|
385
|
+
initializer=_initialize_outline_worker,
|
|
386
|
+
initargs=(worker_context,),
|
|
387
|
+
)
|
|
388
|
+
executor = active_executor
|
|
389
|
+
|
|
390
|
+
def submit(task: OutlineTask) -> Future[OutlineResult]:
|
|
391
|
+
if worker_context is None:
|
|
392
|
+
return active_executor.submit(operation, task)
|
|
393
|
+
return active_executor.submit(
|
|
394
|
+
_parse_outline_work_item,
|
|
395
|
+
_OutlineWorkItem(
|
|
396
|
+
task.ordinal,
|
|
397
|
+
task.source_path,
|
|
398
|
+
task.return_text,
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
task_iterator = iter(_largest_sources_first(pending_tasks))
|
|
403
|
+
tasks_exhausted = False
|
|
404
|
+
|
|
405
|
+
def desired_window() -> int:
|
|
406
|
+
if active_worker_limit is None:
|
|
407
|
+
return dispatch_workers
|
|
408
|
+
return max(1, min(dispatch_workers, active_worker_limit()))
|
|
409
|
+
|
|
410
|
+
def fill_submission_window() -> None:
|
|
411
|
+
nonlocal tasks_exhausted
|
|
412
|
+
while not tasks_exhausted and len(futures) < desired_window():
|
|
413
|
+
try:
|
|
414
|
+
task = next(task_iterator)
|
|
415
|
+
except StopIteration:
|
|
416
|
+
tasks_exhausted = True
|
|
417
|
+
return
|
|
418
|
+
futures[submit(task)] = task
|
|
419
|
+
|
|
420
|
+
fill_submission_window()
|
|
421
|
+
next_heartbeat = monotonic()
|
|
422
|
+
if heartbeat is not None:
|
|
423
|
+
(force_heartbeat or heartbeat)()
|
|
424
|
+
next_heartbeat = monotonic() + 0.5
|
|
425
|
+
while futures:
|
|
426
|
+
if heartbeat is None:
|
|
427
|
+
future = next(iter(as_completed(tuple(futures))))
|
|
428
|
+
else:
|
|
429
|
+
while True:
|
|
430
|
+
timeout = max(0.0, next_heartbeat - monotonic())
|
|
431
|
+
completed, _pending = wait(
|
|
432
|
+
tuple(futures),
|
|
433
|
+
timeout=timeout,
|
|
434
|
+
return_when=FIRST_COMPLETED,
|
|
435
|
+
)
|
|
436
|
+
now = monotonic()
|
|
437
|
+
if now >= next_heartbeat:
|
|
438
|
+
heartbeat()
|
|
439
|
+
next_heartbeat = now + 0.5
|
|
440
|
+
if completed:
|
|
441
|
+
future = next(iter(completed))
|
|
442
|
+
break
|
|
443
|
+
task = futures.pop(future)
|
|
444
|
+
if heartbeat is not None and parallel_completed == 0:
|
|
445
|
+
(force_heartbeat or heartbeat)()
|
|
446
|
+
try:
|
|
447
|
+
result = future.result()
|
|
448
|
+
except ParallelOutlineError:
|
|
449
|
+
raise
|
|
450
|
+
except Exception as error:
|
|
451
|
+
wrapped = ParallelOutlineError(
|
|
452
|
+
f"parallel outline execution failed for {task.source_path}: {error}"
|
|
453
|
+
)
|
|
454
|
+
if configured_workers == 0 and parallel_completed == 0:
|
|
455
|
+
_cancel_futures(futures)
|
|
456
|
+
_shutdown_failed_executor(executor)
|
|
457
|
+
results, serial_completed = _run_serial(
|
|
458
|
+
pending_tasks,
|
|
459
|
+
complete,
|
|
460
|
+
retain_results,
|
|
461
|
+
operation,
|
|
462
|
+
)
|
|
463
|
+
accepted.extend(results)
|
|
464
|
+
_prune_artifacts(prepared)
|
|
465
|
+
return _batch(
|
|
466
|
+
configured_workers,
|
|
467
|
+
1,
|
|
468
|
+
accepted,
|
|
469
|
+
started,
|
|
470
|
+
files_completed=cached_completed + serial_completed,
|
|
471
|
+
fallbacks=1,
|
|
472
|
+
)
|
|
473
|
+
raise wrapped from error
|
|
474
|
+
parallel_completed += 1
|
|
475
|
+
complete(result)
|
|
476
|
+
if retain_results:
|
|
477
|
+
accepted.append(result)
|
|
478
|
+
fill_submission_window()
|
|
479
|
+
except ParallelOutlineError:
|
|
480
|
+
_cancel_futures(futures)
|
|
481
|
+
_shutdown_failed_executor(executor)
|
|
482
|
+
raise
|
|
483
|
+
except (OSError, BrokenProcessPool) as error:
|
|
484
|
+
_cancel_futures(futures)
|
|
485
|
+
_shutdown_failed_executor(executor)
|
|
486
|
+
if configured_workers == 0 and parallel_completed == 0:
|
|
487
|
+
results, serial_completed = _run_serial(
|
|
488
|
+
pending_tasks,
|
|
489
|
+
complete,
|
|
490
|
+
retain_results,
|
|
491
|
+
operation,
|
|
492
|
+
)
|
|
493
|
+
accepted.extend(results)
|
|
494
|
+
_prune_artifacts(prepared)
|
|
495
|
+
return _batch(
|
|
496
|
+
configured_workers,
|
|
497
|
+
1,
|
|
498
|
+
accepted,
|
|
499
|
+
started,
|
|
500
|
+
files_completed=cached_completed + serial_completed,
|
|
501
|
+
fallbacks=1,
|
|
502
|
+
)
|
|
503
|
+
raise ParallelOutlineError(
|
|
504
|
+
f"parallel outline execution failed: {error}"
|
|
505
|
+
) from error
|
|
506
|
+
except BaseException:
|
|
507
|
+
_cancel_futures(futures)
|
|
508
|
+
_shutdown_failed_executor(executor)
|
|
509
|
+
raise
|
|
510
|
+
|
|
511
|
+
assert executor is not None
|
|
512
|
+
executor.shutdown(wait=True, cancel_futures=False)
|
|
513
|
+
_prune_artifacts(prepared)
|
|
514
|
+
|
|
515
|
+
return _batch(
|
|
516
|
+
configured_workers,
|
|
517
|
+
effective_workers,
|
|
518
|
+
accepted,
|
|
519
|
+
started,
|
|
520
|
+
files_completed=cached_completed + parallel_completed,
|
|
521
|
+
fallbacks=0,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _prepare_artifacts(
|
|
526
|
+
tasks: tuple[OutlineTask, ...],
|
|
527
|
+
) -> tuple[
|
|
528
|
+
list[OutlineResult],
|
|
529
|
+
tuple[OutlineTask, ...],
|
|
530
|
+
dict[tuple[int, str], _PreparedArtifact],
|
|
531
|
+
]:
|
|
532
|
+
cached_results: list[OutlineResult] = []
|
|
533
|
+
pending_tasks: list[OutlineTask] = []
|
|
534
|
+
prepared: dict[tuple[int, str], _PreparedArtifact] = {}
|
|
535
|
+
stores: dict[str, ArtifactStore] = {}
|
|
536
|
+
for task in tasks:
|
|
537
|
+
if not task.workspace_root:
|
|
538
|
+
pending_tasks.append(task)
|
|
539
|
+
continue
|
|
540
|
+
text = ""
|
|
541
|
+
cached: CachedOutlineArtifact | None = None
|
|
542
|
+
cache_check_succeeded = False
|
|
543
|
+
with contextlib.suppress(Exception):
|
|
544
|
+
text = read_source_text(Path(task.source_path))
|
|
545
|
+
key = artifact_key(
|
|
546
|
+
task.workspace_root,
|
|
547
|
+
task.source_path,
|
|
548
|
+
text,
|
|
549
|
+
task.defines,
|
|
550
|
+
include_paths=task.include_paths,
|
|
551
|
+
project_config=task.project_config,
|
|
552
|
+
producer_version=task.producer_version,
|
|
553
|
+
)
|
|
554
|
+
store = stores.setdefault(
|
|
555
|
+
task.workspace_root,
|
|
556
|
+
ArtifactStore(Path(task.workspace_root) / ".delphi-lsp" / "artifacts"),
|
|
557
|
+
)
|
|
558
|
+
prepared[(task.ordinal, task.source_path)] = _PreparedArtifact(store, key)
|
|
559
|
+
cached = load_outline_artifact(store, key, task.source_path)
|
|
560
|
+
cache_check_succeeded = True
|
|
561
|
+
if not cache_check_succeeded:
|
|
562
|
+
pending_tasks.append(task)
|
|
563
|
+
continue
|
|
564
|
+
if cached is None:
|
|
565
|
+
pending_tasks.append(task)
|
|
566
|
+
continue
|
|
567
|
+
cached_results.append(
|
|
568
|
+
OutlineResult(
|
|
569
|
+
ordinal=task.ordinal,
|
|
570
|
+
source_path=task.source_path,
|
|
571
|
+
text=text if task.return_text else "",
|
|
572
|
+
model=cached.model,
|
|
573
|
+
lines_processed=cached.lines_processed,
|
|
574
|
+
symbols_discovered=cached.symbols_discovered,
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
return cached_results, tuple(pending_tasks), prepared
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _prune_artifacts(
|
|
581
|
+
prepared: dict[tuple[int, str], _PreparedArtifact],
|
|
582
|
+
) -> None:
|
|
583
|
+
live_keys_by_root: dict[Path, set[str]] = {}
|
|
584
|
+
stores: dict[Path, ArtifactStore] = {}
|
|
585
|
+
for artifact in prepared.values():
|
|
586
|
+
live_keys_by_root.setdefault(artifact.store.root, set()).add(
|
|
587
|
+
artifact.key.cache_key
|
|
588
|
+
)
|
|
589
|
+
stores[artifact.store.root] = artifact.store
|
|
590
|
+
for root, live_keys in live_keys_by_root.items():
|
|
591
|
+
with contextlib.suppress(Exception):
|
|
592
|
+
stores[root].prune(live_keys)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _largest_sources_first(tasks: tuple[OutlineTask, ...]) -> tuple[OutlineTask, ...]:
|
|
596
|
+
sizes: dict[str, int] = {}
|
|
597
|
+
for task in tasks:
|
|
598
|
+
try:
|
|
599
|
+
sizes[task.source_path] = Path(task.source_path).stat().st_size
|
|
600
|
+
except OSError:
|
|
601
|
+
sizes[task.source_path] = -1
|
|
602
|
+
return tuple(
|
|
603
|
+
sorted(
|
|
604
|
+
tasks,
|
|
605
|
+
key=lambda task: (
|
|
606
|
+
-sizes[task.source_path],
|
|
607
|
+
task.ordinal,
|
|
608
|
+
),
|
|
609
|
+
)
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _run_serial(
|
|
614
|
+
tasks: tuple[OutlineTask, ...],
|
|
615
|
+
on_complete: Callable[[OutlineResult], None] | None,
|
|
616
|
+
retain_results: bool,
|
|
617
|
+
operation: Callable[[OutlineTask], OutlineResult],
|
|
618
|
+
) -> tuple[list[OutlineResult], int]:
|
|
619
|
+
results = []
|
|
620
|
+
for task in tasks:
|
|
621
|
+
result = operation(task)
|
|
622
|
+
if on_complete is not None:
|
|
623
|
+
on_complete(result)
|
|
624
|
+
if retain_results:
|
|
625
|
+
results.append(result)
|
|
626
|
+
return results, len(tasks)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _cancel_futures(futures: dict[Future[OutlineResult], OutlineTask]) -> None:
|
|
630
|
+
for future in futures:
|
|
631
|
+
future.cancel()
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _shutdown_failed_executor(executor: ProcessPoolExecutor | None) -> None:
|
|
635
|
+
if executor is None:
|
|
636
|
+
return
|
|
637
|
+
try:
|
|
638
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
639
|
+
except BaseException:
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _batch(
|
|
644
|
+
configured_workers: int,
|
|
645
|
+
effective_workers: int,
|
|
646
|
+
results: list[OutlineResult],
|
|
647
|
+
started: float,
|
|
648
|
+
*,
|
|
649
|
+
files_completed: int,
|
|
650
|
+
fallbacks: int,
|
|
651
|
+
) -> OutlineBatch:
|
|
652
|
+
ordered_results = tuple(sorted(results, key=lambda result: result.ordinal))
|
|
653
|
+
return OutlineBatch(
|
|
654
|
+
results=ordered_results,
|
|
655
|
+
stats=ParallelBuildStats(
|
|
656
|
+
configured_workers=configured_workers,
|
|
657
|
+
effective_workers=effective_workers,
|
|
658
|
+
files_completed=files_completed,
|
|
659
|
+
elapsed_seconds=perf_counter() - started,
|
|
660
|
+
fallbacks=fallbacks,
|
|
661
|
+
),
|
|
662
|
+
)
|