perf(server): avoid unnecessary reparses and cache navigation definitions
Introduce several changes to reduce full-document reparses, lock contention and redundant work when handling TclOO analysis and navigation: - Add a cheap may_contain_classes pre-check and several cursor/receiver heuristics so completions, signature help and tcloo definitions skip the expensive marker reparse when the document cannot contain useful OO info. - Allow passing an existing parsed tree into tcloo completion/signature/definition helpers; update callers to use the server's cached tree when available. - Use a thread-local parser for request-time parse_source to avoid blocking the shared parser during background indexing, and add navigation_state() which returns cached definition identities (invalidated on index generation changes). - Add a cheap name pre-filter (_may_resolve_to) for symbol matching and only run class highlighting when classes may exist. These changes reduce contention and repeated parsing, improve responsiveness for requests during background indexing, and cache navigation definition identities. Tests were added/updated to assert caching and non-blocking behavior.
This commit is contained in:
@@ -20,7 +20,12 @@ from tools.tcloo_completion import indexed_classes
|
||||
from tools.file_sourcing import get_all_psc_files, psc_script_files
|
||||
from tools.formatter import NxFormatter as Formatter
|
||||
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
|
||||
from tools.navigation import FileSymbolIndex, build_file_symbol_index
|
||||
from tools.navigation import (
|
||||
FileSymbolIndex,
|
||||
SymbolIdentity,
|
||||
build_file_symbol_index,
|
||||
definition_identities,
|
||||
)
|
||||
from tools.proc_docs import build_proc_docs
|
||||
from tools.variable_index import ProcRange, build_variable_index
|
||||
|
||||
@@ -54,6 +59,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self._ast_cache = {}
|
||||
self._line_cache: dict[tuple[str, int | None], tuple[str, ...]] = {}
|
||||
self._parser_lock = threading.RLock()
|
||||
self._thread_parsers = threading.local()
|
||||
self._index_lock = threading.RLock()
|
||||
self._index_tokens: dict[str, int] = {}
|
||||
self._index_versions: dict[str, int | None] = {}
|
||||
@@ -67,6 +73,9 @@ class TclLanguageServer(LanguageServer):
|
||||
-1,
|
||||
frozenset(),
|
||||
)
|
||||
self._definition_identities_cache: tuple[
|
||||
int, frozenset[SymbolIdentity]
|
||||
] = (-1, frozenset())
|
||||
self._proc_metadata_cache: dict[
|
||||
str, tuple[int, dict[str, list[str]], dict[str, str]]
|
||||
] = {}
|
||||
@@ -84,10 +93,17 @@ class TclLanguageServer(LanguageServer):
|
||||
return tree, list(self.parser.violations)
|
||||
|
||||
def parse_source(self, source: str):
|
||||
"""Parse without retaining an AST, serialized around the shared parser."""
|
||||
with self._parser_lock:
|
||||
tree, _ = self._parse_source(source)
|
||||
return tree
|
||||
"""Parse without retaining an AST, on a parser owned by this thread.
|
||||
|
||||
Background indexing must not hold the shared parser lock for whole
|
||||
files while request handlers wait for their document's tree.
|
||||
"""
|
||||
local_parser = getattr(self._thread_parsers, "parser", None)
|
||||
if local_parser is None:
|
||||
# Plugin commands live in tclint's shared registry, see __init__.
|
||||
local_parser = self._thread_parsers.parser = parser.CustomParser()
|
||||
local_parser.violations = []
|
||||
return local_parser.parse(source)
|
||||
|
||||
def get_tree(self, document: TextDocument):
|
||||
key = (document.uri, document.version)
|
||||
@@ -154,6 +170,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self._index_generation += 1
|
||||
self._workspace_completion_cache = (-1, ())
|
||||
self._custom_function_names_cache = (-1, frozenset())
|
||||
self._definition_identities_cache = (-1, frozenset())
|
||||
self._proc_metadata_cache.clear()
|
||||
self._custom_inlay_cache.clear()
|
||||
|
||||
@@ -505,6 +522,22 @@ class TclLanguageServer(LanguageServer):
|
||||
with self._index_lock:
|
||||
return dict(self.navigation_indexes)
|
||||
|
||||
def navigation_state(
|
||||
self,
|
||||
) -> tuple[dict[str, FileSymbolIndex], frozenset[SymbolIdentity]]:
|
||||
"""Return indexes plus their definitions, cached by index generation."""
|
||||
with self._index_lock:
|
||||
generation, definitions = self._definition_identities_cache
|
||||
if generation != self._index_generation:
|
||||
definitions = frozenset(
|
||||
definition_identities(self.navigation_indexes)
|
||||
)
|
||||
self._definition_identities_cache = (
|
||||
self._index_generation,
|
||||
definitions,
|
||||
)
|
||||
return dict(self.navigation_indexes), definitions
|
||||
|
||||
def _begin_index_update(self, filepath: str, version: int | None) -> int | None:
|
||||
with self._index_lock:
|
||||
indexed_version = self._index_versions.get(filepath)
|
||||
|
||||
Reference in New Issue
Block a user