python-delphi-lsp 2.1.0__py3-none-any.whl → 2.2.0__py3-none-any.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/_version.py +1 -1
- delphi_lsp/agent_cache.py +46 -6
- delphi_lsp/agent_context.py +323 -33
- delphi_lsp/agent_relations.py +28 -3
- delphi_lsp/agent_workspace.py +35 -0
- delphi_lsp/delphiast_lexer.py +284 -0
- delphi_lsp/delphiast_parser.py +1498 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/metrics.py +35 -1
- delphi_lsp/parallel_outline.py +49 -10
- delphi_lsp/parser.py +54 -4
- delphi_lsp/parser_backend.py +57 -0
- delphi_lsp/project_indexer.py +32 -1
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.2.0.dist-info}/METADATA +21 -11
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.2.0.dist-info}/RECORD +19 -15
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.2.0.dist-info}/WHEEL +0 -0
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.2.0.dist-info}/entry_points.txt +0 -0
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {python_delphi_lsp-2.1.0.dist-info → python_delphi_lsp-2.2.0.dist-info}/top_level.txt +0 -0
delphi_lsp/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.
|
|
1
|
+
__version__ = "2.2.0"
|
delphi_lsp/agent_cache.py
CHANGED
|
@@ -20,6 +20,8 @@ import threading
|
|
|
20
20
|
import time
|
|
21
21
|
from types import ModuleType
|
|
22
22
|
|
|
23
|
+
from watchfiles import watch
|
|
24
|
+
|
|
23
25
|
from ._version import __version__
|
|
24
26
|
from .agent_context import AgentContext
|
|
25
27
|
from .agent_protocol import AgentProtocolError
|
|
@@ -32,10 +34,15 @@ DEFAULT_IDLE_TIMEOUT = 1800
|
|
|
32
34
|
DEFAULT_STARTUP_TIMEOUT = 120.0
|
|
33
35
|
_MAX_MESSAGE_BYTES = 1024 * 1024
|
|
34
36
|
_CONNECTION_TIMEOUT = 2.0
|
|
37
|
+
_CLIENT_RESPONSE_TIMEOUT = 120.0
|
|
35
38
|
_MEMORY_SIZE = re.compile(r"^(?P<count>[1-9][0-9]*)(?P<suffix>[KMG]?)$", re.IGNORECASE)
|
|
36
39
|
_STARTUP_DIAGNOSTIC_BYTES = 16 * 1024
|
|
37
40
|
_STARTUP_TOKEN_RE = re.compile(r"(?i)(token\b[^\n\r]*?:?\s*['\"]?[A-Za-z0-9_-]+['\"]?|\b[a-zA-Z0-9_-]{32,})")
|
|
38
41
|
_START_LOCK_INCOMPLETE_GRACE_SECONDS = 1.0
|
|
42
|
+
_CACHE_REVISION_CHECK_INTERVAL_SECONDS = 3600.0
|
|
43
|
+
_WATCHED_SUFFIXES = frozenset(
|
|
44
|
+
{".pas", ".pp", ".inc", ".dpr", ".dpk", ".dproj", ".cfg"}
|
|
45
|
+
)
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
def estimate_deep_size(value: object) -> int:
|
|
@@ -475,7 +482,7 @@ def _start_lock(root: str | Path, timeout: float):
|
|
|
475
482
|
def _client_exchange(metadata: CacheMetadata, request: dict[str, object]) -> CacheClientResponse:
|
|
476
483
|
try:
|
|
477
484
|
with socket.create_connection(("127.0.0.1", metadata.port), timeout=2) as connection:
|
|
478
|
-
connection.settimeout(
|
|
485
|
+
connection.settimeout(_CLIENT_RESPONSE_TIMEOUT)
|
|
479
486
|
request_without_token = {key: value for key, value in request.items() if key != "token"}
|
|
480
487
|
connection.sendall(json.dumps({"token": metadata.token, **request_without_token}, separators=(",", ":")).encode("utf-8") + b"\n")
|
|
481
488
|
response = _read_line(connection)
|
|
@@ -541,6 +548,7 @@ class _CacheService:
|
|
|
541
548
|
metadata.project_file or None,
|
|
542
549
|
workers=metadata.workers,
|
|
543
550
|
worker_memory_budget_bytes=metadata.max_memory_bytes,
|
|
551
|
+
revision_check_interval_seconds=_CACHE_REVISION_CHECK_INTERVAL_SECONDS,
|
|
544
552
|
)
|
|
545
553
|
self.budget = CacheBudget(metadata.max_memory_bytes)
|
|
546
554
|
self.stats = CacheStats()
|
|
@@ -556,15 +564,14 @@ class _CacheService:
|
|
|
556
564
|
def prewarm(self) -> None:
|
|
557
565
|
started = time.monotonic()
|
|
558
566
|
try:
|
|
559
|
-
self.context.
|
|
560
|
-
self.last_revision = self.context.workspace.workspace_revision
|
|
567
|
+
self.last_revision = self.context.prewarm_navigation()
|
|
561
568
|
self.cache_state = "warm"
|
|
562
569
|
except AgentProtocolError as error:
|
|
563
570
|
if error.code != "project_required":
|
|
564
571
|
raise
|
|
565
572
|
self.cache_state = "ready"
|
|
566
573
|
self.last_budget = self.budget.enforce(
|
|
567
|
-
measure=lambda:
|
|
574
|
+
measure=lambda: self.context.estimated_cache_bytes,
|
|
568
575
|
evict_auxiliary=self.context.evict_auxiliary_caches,
|
|
569
576
|
evict_navigation=self.context.evict_navigation_caches,
|
|
570
577
|
)
|
|
@@ -605,7 +612,7 @@ class _CacheService:
|
|
|
605
612
|
if before != after:
|
|
606
613
|
self.stats.invalidations += 1
|
|
607
614
|
self.last_budget = self.budget.enforce(
|
|
608
|
-
measure=lambda:
|
|
615
|
+
measure=lambda: self.context.estimated_cache_bytes,
|
|
609
616
|
evict_auxiliary=self.context.evict_auxiliary_caches,
|
|
610
617
|
evict_navigation=self.context.evict_navigation_caches,
|
|
611
618
|
)
|
|
@@ -642,10 +649,31 @@ class _CacheService:
|
|
|
642
649
|
"parallel_seconds": self.context.parallel_stats.elapsed_seconds,
|
|
643
650
|
"parallel_fallbacks": self.stats.parallel_fallbacks,
|
|
644
651
|
"idle_timeout": self.metadata.idle_timeout, "idle_remaining": max(0.0, self.metadata.idle_timeout - idle),
|
|
645
|
-
"workspace_revision": self.
|
|
652
|
+
"workspace_revision": self.last_revision,
|
|
646
653
|
}
|
|
647
654
|
|
|
648
655
|
|
|
656
|
+
def _watch_filter(_change: object, path: str) -> bool:
|
|
657
|
+
return Path(path).suffix.casefold() in _WATCHED_SUFFIXES
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _watch_workspace(service: _CacheService) -> None:
|
|
661
|
+
try:
|
|
662
|
+
for changes in watch(
|
|
663
|
+
service.metadata.root,
|
|
664
|
+
watch_filter=_watch_filter,
|
|
665
|
+
stop_event=service.shutdown,
|
|
666
|
+
debounce=50,
|
|
667
|
+
step=20,
|
|
668
|
+
recursive=True,
|
|
669
|
+
raise_interrupt=False,
|
|
670
|
+
):
|
|
671
|
+
if changes:
|
|
672
|
+
service.context.invalidate_revision_cache()
|
|
673
|
+
except (OSError, RuntimeError):
|
|
674
|
+
service.context.invalidate_revision_cache()
|
|
675
|
+
|
|
676
|
+
|
|
649
677
|
def _serve_connection(connection: socket.socket, service: _CacheService) -> None:
|
|
650
678
|
try:
|
|
651
679
|
line = _read_line(connection)
|
|
@@ -691,9 +719,17 @@ def run_cache_daemon(
|
|
|
691
719
|
idle_timeout,
|
|
692
720
|
time.time(),
|
|
693
721
|
)
|
|
722
|
+
watcher: threading.Thread | None = None
|
|
694
723
|
try:
|
|
695
724
|
service = _CacheService(metadata)
|
|
696
725
|
service.prewarm()
|
|
726
|
+
watcher = threading.Thread(
|
|
727
|
+
target=_watch_workspace,
|
|
728
|
+
args=(service,),
|
|
729
|
+
name="delphi-cache-watcher",
|
|
730
|
+
daemon=True,
|
|
731
|
+
)
|
|
732
|
+
watcher.start()
|
|
697
733
|
_write_metadata(metadata)
|
|
698
734
|
while not service.shutdown.is_set() and time.monotonic() - service.last_activity < idle_timeout:
|
|
699
735
|
try:
|
|
@@ -704,6 +740,10 @@ def run_cache_daemon(
|
|
|
704
740
|
connection.settimeout(_CONNECTION_TIMEOUT)
|
|
705
741
|
_serve_connection(connection, service)
|
|
706
742
|
finally:
|
|
743
|
+
if "service" in locals():
|
|
744
|
+
service.shutdown.set()
|
|
745
|
+
if watcher is not None:
|
|
746
|
+
watcher.join(timeout=2.0)
|
|
707
747
|
listener.close()
|
|
708
748
|
_remove_metadata_if_owned(metadata)
|
|
709
749
|
|
delphi_lsp/agent_context.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from bisect import bisect_left, bisect_right
|
|
4
|
+
from collections import OrderedDict
|
|
4
5
|
from collections.abc import Mapping
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
+
from dataclasses import dataclass, replace
|
|
6
7
|
from heapq import heappop, heappush
|
|
7
8
|
import hashlib
|
|
8
9
|
import json
|
|
9
10
|
from pathlib import Path, PureWindowsPath
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
10
13
|
import unicodedata
|
|
11
14
|
|
|
12
15
|
from .agent_protocol import (
|
|
@@ -22,12 +25,17 @@ from .agent_metrics import build_workspace_metrics, project_metric_item, unit_me
|
|
|
22
25
|
from .agent_relations import ProjectRelationIndex, RelationTarget
|
|
23
26
|
from .agent_workspace import AgentUnit, AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
|
|
24
27
|
from .consts import AttributeName, SyntaxNodeType
|
|
25
|
-
from .lsp_server import multiline_string_block_end
|
|
28
|
+
from .lsp_server import build_outline_semantic_model, multiline_string_block_end
|
|
26
29
|
from .nodes import CompoundSyntaxNode, SyntaxNode
|
|
27
30
|
from .parser import DelphiParser
|
|
28
|
-
from .semantic import Scope, ScopeKind, Symbol, SymbolKind
|
|
31
|
+
from .semantic import NamedTypeRef, Scope, ScopeKind, Symbol, SymbolKind
|
|
29
32
|
from .metrics import ProjectMetrics
|
|
30
|
-
from .parallel_outline import
|
|
33
|
+
from .parallel_outline import (
|
|
34
|
+
ParallelBuildStats,
|
|
35
|
+
ParallelOutlineError,
|
|
36
|
+
run_outline_tasks,
|
|
37
|
+
)
|
|
38
|
+
from .source_reader import read_source_text
|
|
31
39
|
|
|
32
40
|
|
|
33
41
|
_ROUTINE_KINDS = frozenset(
|
|
@@ -90,9 +98,10 @@ _CALLING_CONVENTIONS = frozenset(
|
|
|
90
98
|
{"cdecl", "pascal", "register", "safecall", "stdcall", "winapi"}
|
|
91
99
|
)
|
|
92
100
|
_SOURCE_CHUNK_CHARS = 6000
|
|
101
|
+
_RANKED_QUERY_CACHE_SIZE = 16
|
|
93
102
|
|
|
94
103
|
|
|
95
|
-
@dataclass(frozen=True)
|
|
104
|
+
@dataclass(frozen=True, slots=True)
|
|
96
105
|
class _Token:
|
|
97
106
|
value: str
|
|
98
107
|
start: int
|
|
@@ -137,6 +146,18 @@ class _SourceDocument:
|
|
|
137
146
|
self.parser_spans: dict[str, tuple[int, int] | None] = {}
|
|
138
147
|
self._full_parse_attempted = False
|
|
139
148
|
self._full_parse_result: object | None = None
|
|
149
|
+
self.retained_bytes = (
|
|
150
|
+
sys.getsizeof(self)
|
|
151
|
+
+ sys.getsizeof(self.text)
|
|
152
|
+
+ sys.getsizeof(self.line_starts)
|
|
153
|
+
+ len(self.line_starts) * 32
|
|
154
|
+
+ sys.getsizeof(self.tokens)
|
|
155
|
+
+ len(self.tokens) * 160
|
|
156
|
+
+ sys.getsizeof(self.token_starts)
|
|
157
|
+
+ len(self.token_starts) * 28
|
|
158
|
+
+ sys.getsizeof(self.directive_starts)
|
|
159
|
+
+ len(self.directive_starts) * 28
|
|
160
|
+
)
|
|
140
161
|
|
|
141
162
|
def offset(self, line: int, column: int = 1) -> int:
|
|
142
163
|
if not self.line_starts:
|
|
@@ -185,7 +206,78 @@ class _SourceDocument:
|
|
|
185
206
|
return self._full_parse_result
|
|
186
207
|
|
|
187
208
|
|
|
188
|
-
@dataclass(frozen=True)
|
|
209
|
+
@dataclass(frozen=True, slots=True)
|
|
210
|
+
class _SourceSpec:
|
|
211
|
+
source_path: Path
|
|
212
|
+
display_path: str
|
|
213
|
+
defines: tuple[str, ...]
|
|
214
|
+
include_paths: tuple[str, ...]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class _SourceStore:
|
|
218
|
+
"""Load expensive tokenized source documents only when source evidence is requested."""
|
|
219
|
+
|
|
220
|
+
def __init__(
|
|
221
|
+
self,
|
|
222
|
+
specs: Mapping[Path, _SourceSpec],
|
|
223
|
+
*,
|
|
224
|
+
max_loaded_bytes: int,
|
|
225
|
+
) -> None:
|
|
226
|
+
self._specs = dict(specs)
|
|
227
|
+
self._loaded: OrderedDict[Path, _SourceDocument] = OrderedDict()
|
|
228
|
+
self._loaded_bytes = 0
|
|
229
|
+
self._max_loaded_bytes = max(0, max_loaded_bytes)
|
|
230
|
+
|
|
231
|
+
def __getitem__(self, source_path: Path) -> _SourceDocument:
|
|
232
|
+
cached = self._loaded.pop(source_path, None)
|
|
233
|
+
if cached is not None:
|
|
234
|
+
self._loaded[source_path] = cached
|
|
235
|
+
return cached
|
|
236
|
+
|
|
237
|
+
spec = self._specs[source_path]
|
|
238
|
+
document = _SourceDocument(
|
|
239
|
+
spec.source_path,
|
|
240
|
+
spec.display_path,
|
|
241
|
+
read_source_text(spec.source_path),
|
|
242
|
+
defines=spec.defines,
|
|
243
|
+
include_paths=spec.include_paths,
|
|
244
|
+
)
|
|
245
|
+
if document.retained_bytes > self._max_loaded_bytes:
|
|
246
|
+
return document
|
|
247
|
+
while (
|
|
248
|
+
self._loaded
|
|
249
|
+
and self._loaded_bytes + document.retained_bytes > self._max_loaded_bytes
|
|
250
|
+
):
|
|
251
|
+
_, evicted = self._loaded.popitem(last=False)
|
|
252
|
+
self._loaded_bytes -= evicted.retained_bytes
|
|
253
|
+
self._loaded[source_path] = document
|
|
254
|
+
self._loaded_bytes += document.retained_bytes
|
|
255
|
+
return document
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def loaded_count(self) -> int:
|
|
259
|
+
return len(self._loaded)
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def retained_bytes(self) -> int:
|
|
263
|
+
return self._loaded_bytes
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def metadata_bytes(self) -> int:
|
|
267
|
+
return 512 + sum(
|
|
268
|
+
256
|
|
269
|
+
+ sys.getsizeof(path)
|
|
270
|
+
+ sys.getsizeof(spec)
|
|
271
|
+
+ sys.getsizeof(spec.display_path)
|
|
272
|
+
for path, spec in self._specs.items()
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
def clear_loaded(self) -> None:
|
|
276
|
+
self._loaded.clear()
|
|
277
|
+
self._loaded_bytes = 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass(frozen=True, slots=True)
|
|
189
281
|
class _RawSymbol:
|
|
190
282
|
symbol: Symbol
|
|
191
283
|
source_path: Path
|
|
@@ -198,7 +290,115 @@ class _RawSymbol:
|
|
|
198
290
|
signature: str
|
|
199
291
|
|
|
200
292
|
|
|
201
|
-
@dataclass(frozen=True)
|
|
293
|
+
@dataclass(frozen=True, slots=True)
|
|
294
|
+
class _NavigationTask:
|
|
295
|
+
ordinal: int
|
|
296
|
+
source_path: str
|
|
297
|
+
display_path: str
|
|
298
|
+
unit_name: str
|
|
299
|
+
unit_path: str
|
|
300
|
+
unit_id: str
|
|
301
|
+
unit_has_error: bool
|
|
302
|
+
defines: tuple[str, ...]
|
|
303
|
+
include_paths: tuple[str, ...]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@dataclass(frozen=True, slots=True)
|
|
307
|
+
class _NavigationResult:
|
|
308
|
+
ordinal: int
|
|
309
|
+
source_path: str
|
|
310
|
+
text: str
|
|
311
|
+
model: None
|
|
312
|
+
lines_processed: int
|
|
313
|
+
symbols_discovered: int
|
|
314
|
+
read_error: str
|
|
315
|
+
raw_symbols: tuple[_RawSymbol, ...]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _parse_navigation_task(task: _NavigationTask) -> _NavigationResult:
|
|
319
|
+
source_path = Path(task.source_path)
|
|
320
|
+
try:
|
|
321
|
+
text = read_source_text(source_path)
|
|
322
|
+
except (OSError, UnicodeError) as error:
|
|
323
|
+
return _NavigationResult(
|
|
324
|
+
task.ordinal,
|
|
325
|
+
task.source_path,
|
|
326
|
+
"",
|
|
327
|
+
None,
|
|
328
|
+
0,
|
|
329
|
+
0,
|
|
330
|
+
str(error),
|
|
331
|
+
(),
|
|
332
|
+
)
|
|
333
|
+
try:
|
|
334
|
+
model = build_outline_semantic_model(
|
|
335
|
+
text,
|
|
336
|
+
task.source_path,
|
|
337
|
+
defines=task.defines,
|
|
338
|
+
)
|
|
339
|
+
document = _SourceDocument(
|
|
340
|
+
source_path,
|
|
341
|
+
task.display_path,
|
|
342
|
+
text,
|
|
343
|
+
defines=task.defines,
|
|
344
|
+
include_paths=task.include_paths,
|
|
345
|
+
)
|
|
346
|
+
unit = AgentUnit(
|
|
347
|
+
unit_id=task.unit_id,
|
|
348
|
+
name=task.unit_name,
|
|
349
|
+
path=task.unit_path,
|
|
350
|
+
has_error=task.unit_has_error,
|
|
351
|
+
)
|
|
352
|
+
symbols = _collect_raw_symbols(model.unit_scope, unit, source_path, document)
|
|
353
|
+
raw_symbols = _detach_raw_symbols(
|
|
354
|
+
_exclude_routine_locals(symbols, document),
|
|
355
|
+
task.unit_name,
|
|
356
|
+
)
|
|
357
|
+
except Exception as error:
|
|
358
|
+
raise ParallelOutlineError(
|
|
359
|
+
f"failed to build navigation shard for {task.source_path}: {error}"
|
|
360
|
+
) from error
|
|
361
|
+
return _NavigationResult(
|
|
362
|
+
task.ordinal,
|
|
363
|
+
task.source_path,
|
|
364
|
+
"",
|
|
365
|
+
None,
|
|
366
|
+
text.count("\n") + (0 if not text or text.endswith(("\n", "\r")) else 1),
|
|
367
|
+
len(raw_symbols),
|
|
368
|
+
"",
|
|
369
|
+
raw_symbols,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _detach_raw_symbols(
|
|
374
|
+
raw_symbols: list[_RawSymbol],
|
|
375
|
+
unit_name: str,
|
|
376
|
+
) -> tuple[_RawSymbol, ...]:
|
|
377
|
+
detached_scope = Scope(kind=ScopeKind.UNIT, name=unit_name)
|
|
378
|
+
detached: list[_RawSymbol] = []
|
|
379
|
+
for raw in raw_symbols:
|
|
380
|
+
symbol = raw.symbol
|
|
381
|
+
flat_symbol = Symbol(
|
|
382
|
+
name=symbol.name,
|
|
383
|
+
kind=symbol.kind,
|
|
384
|
+
decl_range=symbol.decl_range,
|
|
385
|
+
name_range=symbol.name_range,
|
|
386
|
+
scope=detached_scope,
|
|
387
|
+
visibility=symbol.visibility,
|
|
388
|
+
type_ref=NamedTypeRef(symbol.type_ref.display_name()),
|
|
389
|
+
modifiers=set(symbol.modifiers),
|
|
390
|
+
attributes=dict(symbol.attributes),
|
|
391
|
+
doc=symbol.doc,
|
|
392
|
+
base_types=tuple(
|
|
393
|
+
NamedTypeRef(base_type.display_name())
|
|
394
|
+
for base_type in symbol.base_types
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
detached.append(replace(raw, symbol=flat_symbol))
|
|
398
|
+
return tuple(detached)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@dataclass(frozen=True, slots=True)
|
|
202
402
|
class _SymbolEntry:
|
|
203
403
|
symbol: Symbol
|
|
204
404
|
source_path: Path
|
|
@@ -228,14 +428,15 @@ class _SymbolEntry:
|
|
|
228
428
|
}
|
|
229
429
|
|
|
230
430
|
|
|
231
|
-
@dataclass(frozen=True)
|
|
431
|
+
@dataclass(frozen=True, slots=True)
|
|
232
432
|
class _Registry:
|
|
233
433
|
project_id: str
|
|
234
434
|
revision: str
|
|
235
435
|
entries: tuple[_SymbolEntry, ...]
|
|
236
436
|
by_target: dict[str, _SymbolEntry]
|
|
237
|
-
sources:
|
|
238
|
-
ranked_queries:
|
|
437
|
+
sources: _SourceStore
|
|
438
|
+
ranked_queries: OrderedDict[str, tuple[_SymbolEntry, ...]]
|
|
439
|
+
static_retained_bytes: int
|
|
239
440
|
|
|
240
441
|
|
|
241
442
|
class AgentContext:
|
|
@@ -245,10 +446,16 @@ class AgentContext:
|
|
|
245
446
|
*,
|
|
246
447
|
workers: int = 0,
|
|
247
448
|
worker_memory_budget_bytes: int | None = None,
|
|
449
|
+
revision_check_interval_seconds: float = 0.0,
|
|
248
450
|
) -> None:
|
|
249
451
|
self._workspace = workspace
|
|
250
452
|
self._workers = workers
|
|
251
453
|
self._worker_memory_budget_bytes = worker_memory_budget_bytes
|
|
454
|
+
self._revision_check_interval_seconds = max(
|
|
455
|
+
0.0,
|
|
456
|
+
revision_check_interval_seconds,
|
|
457
|
+
)
|
|
458
|
+
self._last_revision_check_at = 0.0
|
|
252
459
|
self._parallel_stats = ParallelBuildStats(0, 0, 0, 0.0, 0)
|
|
253
460
|
project_id = workspace.active_project_id
|
|
254
461
|
self._focus = Focus(project_id=project_id) if project_id else Focus()
|
|
@@ -266,11 +473,13 @@ class AgentContext:
|
|
|
266
473
|
*,
|
|
267
474
|
workers: int = 0,
|
|
268
475
|
worker_memory_budget_bytes: int | None = None,
|
|
476
|
+
revision_check_interval_seconds: float = 0.0,
|
|
269
477
|
) -> AgentContext:
|
|
270
478
|
return cls(
|
|
271
479
|
AgentWorkspace.open(root, project_file=project_file),
|
|
272
480
|
workers=workers,
|
|
273
481
|
worker_memory_budget_bytes=worker_memory_budget_bytes,
|
|
482
|
+
revision_check_interval_seconds=revision_check_interval_seconds,
|
|
274
483
|
)
|
|
275
484
|
|
|
276
485
|
@property
|
|
@@ -293,16 +502,46 @@ class AgentContext:
|
|
|
293
502
|
self._metrics,
|
|
294
503
|
)
|
|
295
504
|
|
|
505
|
+
@property
|
|
506
|
+
def estimated_cache_bytes(self) -> int:
|
|
507
|
+
retained = self._workspace.estimated_cache_bytes
|
|
508
|
+
if self._registry is not None:
|
|
509
|
+
retained += (
|
|
510
|
+
self._registry.static_retained_bytes
|
|
511
|
+
+ self._registry.sources.retained_bytes
|
|
512
|
+
+ sum(
|
|
513
|
+
sys.getsizeof(query)
|
|
514
|
+
+ sys.getsizeof(ranked)
|
|
515
|
+
+ len(ranked) * 8
|
|
516
|
+
for query, ranked in self._registry.ranked_queries.items()
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
if self._relation_index is not None:
|
|
520
|
+
retained += max(4096, len(self._registry.entries) * 512) if self._registry else 4096
|
|
521
|
+
if self._metrics is not None:
|
|
522
|
+
retained += 4096 + len(self._metrics.units) * 1024
|
|
523
|
+
return retained
|
|
524
|
+
|
|
296
525
|
def evict_auxiliary_caches(self) -> None:
|
|
297
526
|
self._relation_index = None
|
|
298
527
|
self._metrics = None
|
|
299
528
|
self._metrics_revision = ""
|
|
529
|
+
if self._registry is not None:
|
|
530
|
+
self._registry.sources.clear_loaded()
|
|
300
531
|
|
|
301
532
|
def evict_navigation_caches(self) -> None:
|
|
302
533
|
self.evict_auxiliary_caches()
|
|
303
534
|
self._registry = None
|
|
304
535
|
self._workspace.evict_recomputable_caches()
|
|
305
536
|
|
|
537
|
+
def prewarm_navigation(self) -> str:
|
|
538
|
+
revision = self._refresh_workspace("")
|
|
539
|
+
self._require_registry(revision)
|
|
540
|
+
return revision
|
|
541
|
+
|
|
542
|
+
def invalidate_revision_cache(self) -> None:
|
|
543
|
+
self._last_revision_check_at = 0.0
|
|
544
|
+
|
|
306
545
|
def handle(self, request: AgentRequest | Mapping[str, object]) -> AgentResponse:
|
|
307
546
|
parsed = _validated_request(request)
|
|
308
547
|
revision = self._refresh_workspace(parsed.project_id)
|
|
@@ -328,11 +567,12 @@ class AgentContext:
|
|
|
328
567
|
return self._handle_focus(parsed, revision)
|
|
329
568
|
if parsed.action == "find":
|
|
330
569
|
registry = self._require_registry(revision)
|
|
331
|
-
ranked = registry.ranked_queries.
|
|
570
|
+
ranked = registry.ranked_queries.pop(parsed.query, None)
|
|
332
571
|
if ranked is None:
|
|
333
572
|
ranked = tuple(_ranked_entries(registry.entries, parsed.query))
|
|
334
|
-
|
|
335
|
-
|
|
573
|
+
registry.ranked_queries[parsed.query] = ranked
|
|
574
|
+
while len(registry.ranked_queries) > _RANKED_QUERY_CACHE_SIZE:
|
|
575
|
+
registry.ranked_queries.popitem(last=False)
|
|
336
576
|
items = [entry.card() for entry in ranked]
|
|
337
577
|
return self._response(parsed, revision, items)
|
|
338
578
|
if parsed.action == "inspect":
|
|
@@ -345,10 +585,25 @@ class AgentContext:
|
|
|
345
585
|
def _refresh_workspace(self, requested_project_id: str) -> str:
|
|
346
586
|
previous_project_id = self._workspace.active_project_id
|
|
347
587
|
selected_project_id = requested_project_id or previous_project_id
|
|
348
|
-
|
|
588
|
+
now = time.monotonic()
|
|
589
|
+
selection_changed = bool(
|
|
590
|
+
requested_project_id
|
|
591
|
+
and requested_project_id != previous_project_id
|
|
592
|
+
)
|
|
593
|
+
revision_is_fresh = (
|
|
594
|
+
not selection_changed
|
|
595
|
+
and self._revision_check_interval_seconds > 0.0
|
|
596
|
+
and now - self._last_revision_check_at
|
|
597
|
+
< self._revision_check_interval_seconds
|
|
598
|
+
)
|
|
599
|
+
if revision_is_fresh:
|
|
600
|
+
revision = self._last_revision
|
|
601
|
+
elif selected_project_id:
|
|
349
602
|
revision = self._workspace._select_project_with_revision(selected_project_id)
|
|
350
603
|
else:
|
|
351
604
|
revision = self._workspace.workspace_revision
|
|
605
|
+
if not revision_is_fresh:
|
|
606
|
+
self._last_revision_check_at = now
|
|
352
607
|
current_project_id = self._workspace.active_project_id
|
|
353
608
|
|
|
354
609
|
if current_project_id != previous_project_id:
|
|
@@ -731,36 +986,39 @@ def _build_registry(
|
|
|
731
986
|
worker_memory_budget_bytes: int | None = None,
|
|
732
987
|
) -> tuple[_Registry, ParallelBuildStats]:
|
|
733
988
|
raw_symbols: list[_RawSymbol] = []
|
|
734
|
-
sources: dict[Path, _SourceDocument] = {}
|
|
735
989
|
units = tuple(workspace.units)
|
|
990
|
+
source_specs = {
|
|
991
|
+
unit_source_path(workspace.root, unit): _SourceSpec(
|
|
992
|
+
unit_source_path(workspace.root, unit),
|
|
993
|
+
unit_display_path(workspace.root, unit),
|
|
994
|
+
workspace.defines,
|
|
995
|
+
workspace.include_paths,
|
|
996
|
+
)
|
|
997
|
+
for unit in units
|
|
998
|
+
}
|
|
736
999
|
|
|
737
|
-
def consume_result(result:
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if result.read_error or result.model is None:
|
|
1000
|
+
def consume_result(result: _NavigationResult) -> None:
|
|
1001
|
+
if result.read_error:
|
|
1002
|
+
unit = units[result.ordinal]
|
|
1003
|
+
display_path = unit_display_path(workspace.root, unit)
|
|
742
1004
|
raise AgentProtocolError(
|
|
743
1005
|
"source_unavailable",
|
|
744
1006
|
f"Could not read selected source {display_path}.",
|
|
745
1007
|
)
|
|
746
|
-
|
|
747
|
-
source_path,
|
|
748
|
-
display_path,
|
|
749
|
-
result.text,
|
|
750
|
-
defines=workspace.defines,
|
|
751
|
-
include_paths=workspace.include_paths,
|
|
752
|
-
)
|
|
753
|
-
sources[source_path] = document
|
|
754
|
-
unit_symbols = _collect_raw_symbols(result.model.unit_scope, unit, source_path, document)
|
|
755
|
-
raw_symbols.extend(_exclude_routine_locals(unit_symbols, document))
|
|
1008
|
+
raw_symbols.extend(result.raw_symbols)
|
|
756
1009
|
|
|
757
1010
|
outline_batch = run_outline_tasks(
|
|
758
1011
|
(
|
|
759
|
-
|
|
1012
|
+
_NavigationTask(
|
|
760
1013
|
ordinal,
|
|
761
1014
|
str(unit_source_path(workspace.root, unit)),
|
|
1015
|
+
unit_display_path(workspace.root, unit),
|
|
1016
|
+
unit.name,
|
|
1017
|
+
unit.path,
|
|
1018
|
+
unit.unit_id,
|
|
1019
|
+
unit.has_error,
|
|
762
1020
|
workspace.defines,
|
|
763
|
-
|
|
1021
|
+
workspace.include_paths,
|
|
764
1022
|
)
|
|
765
1023
|
for ordinal, unit in enumerate(units)
|
|
766
1024
|
),
|
|
@@ -768,6 +1026,7 @@ def _build_registry(
|
|
|
768
1026
|
memory_budget_bytes=worker_memory_budget_bytes,
|
|
769
1027
|
on_complete=consume_result,
|
|
770
1028
|
retain_results=False,
|
|
1029
|
+
task_runner=_parse_navigation_task,
|
|
771
1030
|
)
|
|
772
1031
|
|
|
773
1032
|
ordered = sorted(raw_symbols, key=_raw_sort_key)
|
|
@@ -859,6 +1118,9 @@ def _build_registry(
|
|
|
859
1118
|
)
|
|
860
1119
|
|
|
861
1120
|
entries_tuple = tuple(sorted(with_parents, key=_entry_sort_key))
|
|
1121
|
+
source_cache_bytes = _source_cache_budget(worker_memory_budget_bytes)
|
|
1122
|
+
sources = _SourceStore(source_specs, max_loaded_bytes=source_cache_bytes)
|
|
1123
|
+
static_retained_bytes = _estimate_registry_bytes(entries_tuple, sources)
|
|
862
1124
|
return (
|
|
863
1125
|
_Registry(
|
|
864
1126
|
project_id=project_id,
|
|
@@ -866,12 +1128,40 @@ def _build_registry(
|
|
|
866
1128
|
entries=entries_tuple,
|
|
867
1129
|
by_target={entry.target_id: entry for entry in entries_tuple},
|
|
868
1130
|
sources=sources,
|
|
869
|
-
ranked_queries=
|
|
1131
|
+
ranked_queries=OrderedDict(),
|
|
1132
|
+
static_retained_bytes=static_retained_bytes,
|
|
870
1133
|
),
|
|
871
1134
|
outline_batch.stats,
|
|
872
1135
|
)
|
|
873
1136
|
|
|
874
1137
|
|
|
1138
|
+
def _source_cache_budget(total_budget_bytes: int | None) -> int:
|
|
1139
|
+
if total_budget_bytes is None:
|
|
1140
|
+
return 64 * 1024**2
|
|
1141
|
+
return max(0, min(128 * 1024**2, total_budget_bytes // 4))
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def _estimate_registry_bytes(
|
|
1145
|
+
entries: tuple[_SymbolEntry, ...],
|
|
1146
|
+
sources: _SourceStore,
|
|
1147
|
+
) -> int:
|
|
1148
|
+
retained = 4096 + sources.metadata_bytes
|
|
1149
|
+
for entry in entries:
|
|
1150
|
+
retained += (
|
|
1151
|
+
1536
|
|
1152
|
+
+ sys.getsizeof(entry.path)
|
|
1153
|
+
+ sys.getsizeof(entry.unit_id)
|
|
1154
|
+
+ sys.getsizeof(entry.unit_name)
|
|
1155
|
+
+ sys.getsizeof(entry.qualified_name)
|
|
1156
|
+
+ sys.getsizeof(entry.owner)
|
|
1157
|
+
+ sys.getsizeof(entry.signature)
|
|
1158
|
+
+ sys.getsizeof(entry.target_id)
|
|
1159
|
+
+ sys.getsizeof(entry.parent_target_id)
|
|
1160
|
+
)
|
|
1161
|
+
retained += len(entries) * 96
|
|
1162
|
+
return retained
|
|
1163
|
+
|
|
1164
|
+
|
|
875
1165
|
def _stable_path_component(value: str) -> str:
|
|
876
1166
|
normalized = unicodedata.normalize("NFC", value).replace("\\", "_").replace("/", "_")
|
|
877
1167
|
return normalized or "unknown"
|