Skip to content

Commit 065db06

Browse files
committed
perf(langserver): optimize analysing and collecting diagnostics
1 parent 7bde3ab commit 065db06

File tree

9 files changed

+384
-170
lines changed

9 files changed

+384
-170
lines changed

packages/core/src/robotcode/core/text_document.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,16 @@ def cache_invalidated(sender) -> None: ...
215215

216216
@contextmanager
217217
def _cache_invalidating(self) -> Iterator[None]:
218-
self.cache_invalidate()
218+
send_events = len(self._cache) > 0
219+
if send_events:
220+
self.cache_invalidate()
219221
try:
220222
with self._lock:
221223
yield
222224
finally:
223225
self._invalidate_cache()
224-
self.cache_invalidated(self)
226+
if send_events:
227+
self.cache_invalidated(self)
225228

226229
def _invalidate_cache(self) -> None:
227230
self._cache.clear()

packages/language_server/src/robotcode/language_server/common/parts/diagnostics.py

Lines changed: 173 additions & 80 deletions
Large diffs are not rendered by default.

packages/language_server/src/robotcode/language_server/robotframework/parts/diagnostics.py

Lines changed: 74 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import ast
2-
import threading
32
from concurrent.futures import CancelledError
43
from typing import TYPE_CHECKING, Any, List, Optional
54

@@ -24,6 +23,7 @@
2423
GlobalVariableDefinition,
2524
LibraryArgumentDefinition,
2625
)
26+
from robotcode.robot.diagnostics.library_doc import LibraryDoc
2727
from robotcode.robot.diagnostics.namespace import Namespace
2828
from robotcode.robot.utils.ast import (
2929
iter_nodes,
@@ -32,7 +32,7 @@
3232
)
3333
from robotcode.robot.utils.stubs import HasError, HasErrors, HeaderAndBodyBlock
3434

35-
from ...common.parts.diagnostics import DiagnosticsResult
35+
from ...common.parts.diagnostics import DiagnosticsCollectType, DiagnosticsResult
3636

3737
if TYPE_CHECKING:
3838
from ..protocol import RobotLanguageServerProtocol
@@ -58,57 +58,81 @@ def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
5858
self.parent.diagnostics.collect.add(self.collect_unused_keyword_references)
5959
self.parent.diagnostics.collect.add(self.collect_unused_variable_references)
6060

61-
self._collect_unused_references_event = threading.Event()
62-
63-
self.parent.diagnostics.on_workspace_diagnostics_analyze.add(self._on_workspace_diagnostics_analyze)
64-
self.parent.diagnostics.on_workspace_diagnostics_collect.add(self._on_workspace_diagnostics_collect)
61+
self.parent.diagnostics.on_get_related_documents.add(self._on_get_related_documents)
6562

6663
def _on_initialized(self, sender: Any) -> None:
6764
self.parent.diagnostics.analyze.add(self.analyze)
68-
69-
self.parent.documents_cache.namespace_invalidated.add(self.namespace_invalidated)
70-
71-
def _on_workspace_diagnostics_analyze(self, sender: Any) -> None:
72-
self._collect_unused_references_event.clear()
73-
74-
def _on_workspace_diagnostics_collect(self, sender: Any) -> None:
75-
self._collect_unused_references_event.set()
65+
self.parent.documents_cache.namespace_invalidated.add(self._on_namespace_invalidated)
66+
self.parent.documents_cache.namespace_initialized(self._on_namespace_initialized)
67+
self.parent.documents_cache.libraries_changed.add(self._on_libraries_changed)
68+
self.parent.documents_cache.variables_changed.add(self._on_variables_changed)
69+
70+
def _on_libraries_changed(self, sender: Any, libraries: List[LibraryDoc]) -> None:
71+
for doc in self.parent.documents.documents:
72+
namespace = self.parent.documents_cache.get_only_initialized_namespace(doc)
73+
if namespace is not None:
74+
lib_docs = (e.library_doc for e in namespace.get_libraries().values())
75+
if any(lib_doc in lib_docs for lib_doc in libraries):
76+
self.parent.diagnostics.force_refresh_document(doc)
77+
78+
def _on_variables_changed(self, sender: Any, variables: List[LibraryDoc]) -> None:
79+
for doc in self.parent.documents.documents:
80+
namespace = self.parent.documents_cache.get_only_initialized_namespace(doc)
81+
if namespace is not None:
82+
lib_docs = (e.library_doc for e in namespace.get_imported_variables().values())
83+
if any(lib_doc in lib_docs for lib_doc in variables):
84+
self.parent.diagnostics.force_refresh_document(doc)
7685

7786
@language_id("robotframework")
7887
def analyze(self, sender: Any, document: TextDocument) -> None:
7988
self.parent.documents_cache.get_namespace(document).analyze()
8089

8190
@language_id("robotframework")
82-
def namespace_invalidated(self, sender: Any, namespace: Namespace) -> None:
83-
self._collect_unused_references_event.clear()
91+
def _on_namespace_initialized(self, sender: Any, namespace: Namespace) -> None:
92+
if namespace.document is not None:
93+
self.parent.diagnostics.force_refresh_document(namespace.document)
94+
95+
@language_id("robotframework")
96+
def _on_namespace_invalidated(self, sender: Any, namespace: Namespace) -> None:
97+
if namespace.document is not None:
98+
namespace.document.remove_cache_entry(self._collect_model_errors)
99+
namespace.document.remove_cache_entry(self._collect_token_errors)
100+
101+
@language_id("robotframework")
102+
def _on_get_related_documents(self, sender: Any, document: TextDocument) -> Optional[List[TextDocument]]:
103+
namespace = self.parent.documents_cache.get_only_initialized_namespace(document)
104+
if namespace is None:
105+
return None
84106

85-
self._namespace_invalidated(namespace)
107+
result = []
86108

87-
self.parent.diagnostics.break_workspace_diagnostics_loop()
109+
resources = namespace.get_resources().values()
110+
for r in resources:
111+
if r.library_doc.source:
112+
doc = self.parent.documents.get(Uri.from_path(r.library_doc.source).normalized())
113+
if doc is not None:
114+
result.append(doc)
88115

89-
def _namespace_invalidated(self, namespace: Namespace) -> None:
90-
if namespace.document is not None:
91-
refresh = namespace.document.opened_in_editor
116+
lib_doc = namespace.get_library_doc()
117+
for doc in self.parent.documents.documents:
118+
if doc.language_id != "robotframework":
119+
continue
92120

93-
self.parent.diagnostics.force_refresh_document(namespace.document, False)
121+
doc_namespace = self.parent.documents_cache.get_only_initialized_namespace(doc)
122+
if doc_namespace is None:
123+
continue
94124

95-
if namespace.is_initialized():
96-
resources = namespace.get_resources().values()
97-
for r in resources:
98-
if r.library_doc.source:
99-
doc = self.parent.documents.get(Uri.from_path(r.library_doc.source).normalized())
100-
if doc is not None:
101-
refresh |= doc.opened_in_editor
102-
self.parent.diagnostics.force_refresh_document(doc, False)
125+
if doc_namespace.is_analyzed():
126+
for ref in doc_namespace.get_namespace_references():
127+
if ref.library_doc == lib_doc:
128+
result.append(doc)
103129

104-
if refresh:
105-
self.parent.diagnostics.refresh()
130+
return result
106131

107132
@language_id("robotframework")
108-
def collect_namespace_diagnostics(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
109-
return document.get_cache(self._collect_namespace_diagnostics)
110-
111-
def _collect_namespace_diagnostics(self, document: TextDocument) -> DiagnosticsResult:
133+
def collect_namespace_diagnostics(
134+
self, sender: Any, document: TextDocument, diagnostics_type: DiagnosticsCollectType
135+
) -> DiagnosticsResult:
112136
try:
113137
namespace = self.parent.documents_cache.get_namespace(document)
114138

@@ -172,7 +196,9 @@ def _create_error_from_token(self, token: Token, source: Optional[str] = None) -
172196

173197
@language_id("robotframework")
174198
@_logger.call
175-
def collect_token_errors(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
199+
def collect_token_errors(
200+
self, sender: Any, document: TextDocument, diagnostics_type: DiagnosticsCollectType
201+
) -> DiagnosticsResult:
176202
return document.get_cache(self._collect_token_errors)
177203

178204
def _collect_token_errors(self, document: TextDocument) -> DiagnosticsResult:
@@ -238,7 +264,9 @@ def _collect_token_errors(self, document: TextDocument) -> DiagnosticsResult:
238264

239265
@language_id("robotframework")
240266
@_logger.call
241-
def collect_model_errors(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
267+
def collect_model_errors(
268+
self, sender: Any, document: TextDocument, diagnostics_type: DiagnosticsCollectType
269+
) -> DiagnosticsResult:
242270
return document.get_cache(self._collect_model_errors)
243271

244272
def _collect_model_errors(self, document: TextDocument) -> DiagnosticsResult:
@@ -284,13 +312,15 @@ def _collect_model_errors(self, document: TextDocument) -> DiagnosticsResult:
284312

285313
@language_id("robotframework")
286314
@_logger.call
287-
def collect_unused_keyword_references(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
315+
def collect_unused_keyword_references(
316+
self, sender: Any, document: TextDocument, diagnostics_type: DiagnosticsCollectType
317+
) -> DiagnosticsResult:
288318
config = self.parent.workspace.get_configuration(AnalysisConfig, document.uri)
289319

290320
if not config.find_unused_references:
291321
return DiagnosticsResult(self.collect_unused_keyword_references, [])
292322

293-
if not self._collect_unused_references_event.is_set():
323+
if diagnostics_type != DiagnosticsCollectType.SLOW:
294324
return DiagnosticsResult(self.collect_unused_keyword_references, None, True)
295325

296326
return self._collect_unused_keyword_references(document)
@@ -341,13 +371,15 @@ def _collect_unused_keyword_references(self, document: TextDocument) -> Diagnost
341371

342372
@language_id("robotframework")
343373
@_logger.call
344-
def collect_unused_variable_references(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
374+
def collect_unused_variable_references(
375+
self, sender: Any, document: TextDocument, diagnostics_type: DiagnosticsCollectType
376+
) -> DiagnosticsResult:
345377
config = self.parent.workspace.get_configuration(AnalysisConfig, document.uri)
346378

347379
if not config.find_unused_references:
348380
return DiagnosticsResult(self.collect_unused_variable_references, [])
349381

350-
if not self._collect_unused_references_event.is_set():
382+
if diagnostics_type != DiagnosticsCollectType.SLOW:
351383
return DiagnosticsResult(self.collect_unused_variable_references, None, True)
352384

353385
return self._collect_unused_variable_references(document)

packages/language_server/src/robotcode/language_server/robotframework/parts/references.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
6666

6767
parent.references.collect.add(self.collect)
6868
parent.documents.did_change.add(self.document_did_change)
69-
69+
parent.documents.on_document_cache_invalidated(self.document_did_change)
7070
parent.diagnostics.on_workspace_diagnostics_break.add(self.on_workspace_diagnostics_break)
7171

7272
@event

packages/language_server/src/robotcode/language_server/robotframework/parts/robocop_diagnostics.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from robotcode.core.utils.version import Version, create_version_from_str
1515
from robotcode.core.workspace import WorkspaceFolder
1616

17-
from ...common.parts.diagnostics import DiagnosticsResult
17+
from ...common.parts.diagnostics import DiagnosticsCollectType, DiagnosticsResult
1818
from ..configuration import RoboCopConfig
1919
from .protocol_part import RobotLanguageServerProtocolPart
2020

@@ -50,7 +50,9 @@ def get_config(self, document: TextDocument) -> Optional[RoboCopConfig]:
5050

5151
@language_id("robotframework")
5252
@_logger.call
53-
def collect_diagnostics(self, sender: Any, document: TextDocument) -> DiagnosticsResult:
53+
def collect_diagnostics(
54+
self, sender: Any, document: TextDocument, diagnostics_type: DiagnosticsCollectType
55+
) -> DiagnosticsResult:
5456
workspace_folder = self.parent.workspace.get_workspace_folder(document.uri)
5557
if workspace_folder is not None:
5658
extension_config = self.get_config(document)

packages/robot/src/robotcode/robot/diagnostics/document_cache_helper.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from robotcode.core.documents_manager import DocumentsManager
2323
from robotcode.core.event import event
2424
from robotcode.core.filewatcher import FileWatcherManagerBase
25-
from robotcode.core.language import language_id_filter
2625
from robotcode.core.text_document import TextDocument
2726
from robotcode.core.uri import Uri
2827
from robotcode.core.utils.logging import LoggingDescriptor
@@ -32,6 +31,7 @@
3231
from ..utils import get_robot_version
3332
from ..utils.stubs import Languages
3433
from .imports_manager import ImportsManager
34+
from .library_doc import LibraryDoc
3535
from .namespace import DocumentType, Namespace
3636
from .workspace_config import AnalysisRobotConfig, CacheConfig, RobotConfig
3737

@@ -421,22 +421,29 @@ def get_general_namespace(self, document: TextDocument) -> Namespace:
421421
def __get_general_namespace(self, document: TextDocument) -> Namespace:
422422
return self.__get_namespace_for_document_type(document, DocumentType.GENERAL)
423423

424+
@event
425+
def namespace_initialized(sender, namespace: Namespace) -> None: ...
426+
424427
@event
425428
def namespace_invalidated(sender, namespace: Namespace) -> None: ...
426429

427430
def __invalidate_namespace(self, sender: Namespace) -> None:
428431
document = sender.document
429432
if document is not None:
430-
document.invalidate_cache()
433+
document.remove_cache_entry(self.__get_general_namespace)
434+
document.remove_cache_entry(self.__get_init_namespace)
435+
document.remove_cache_entry(self.__get_resource_namespace)
436+
document.remove_cache_entry(self.__get_namespace)
431437

432-
self.namespace_invalidated(self, sender, callback_filter=language_id_filter(document))
438+
self.namespace_invalidated(self, sender)
433439

434440
def __namespace_initialized(self, sender: Namespace) -> None:
435441
if sender.document is not None:
436442
self._logger.debug(
437443
lambda: f"Save initialized Namespace: {sender.document.uri if sender.document else None}"
438444
)
439445
sender.document.set_data(self.INITIALIZED_NAMESPACE, sender)
446+
self.namespace_initialized(self, sender)
440447

441448
def get_initialized_namespace(self, document: TextDocument) -> Namespace:
442449
result: Optional[Namespace] = document.get_data(self.INITIALIZED_NAMESPACE)
@@ -445,6 +452,11 @@ def get_initialized_namespace(self, document: TextDocument) -> Namespace:
445452
result = self.get_namespace(document)
446453
return result
447454

455+
def get_only_initialized_namespace(self, document: TextDocument) -> Optional[Namespace]:
456+
result: Optional[Namespace] = document.get_data(self.INITIALIZED_NAMESPACE)
457+
458+
return result
459+
448460
def __get_namespace_for_document_type(
449461
self, document: TextDocument, document_type: Optional[DocumentType]
450462
) -> Namespace:
@@ -495,7 +507,7 @@ def create_imports_manager(self, root_uri: Uri) -> ImportsManager:
495507
]
496508

497509
analysis_config = self.workspace.get_configuration(AnalysisRobotConfig, root_uri)
498-
return ImportsManager(
510+
result = ImportsManager(
499511
self.documents_manager,
500512
self.file_watcher_manager,
501513
self,
@@ -510,6 +522,30 @@ def create_imports_manager(self, root_uri: Uri) -> ImportsManager:
510522
cache_base_path,
511523
)
512524

525+
result.libraries_changed.add(self._on_libraries_changed)
526+
result.resources_changed.add(self._on_resources_changed)
527+
result.variables_changed.add(self._on_variables_changed)
528+
529+
return result
530+
531+
@event
532+
def libraries_changed(sender, libraries: List[LibraryDoc]) -> None: ...
533+
534+
@event
535+
def resources_changed(sender, resources: List[LibraryDoc]) -> None: ...
536+
537+
@event
538+
def variables_changed(sender, variables: List[LibraryDoc]) -> None: ...
539+
540+
def _on_libraries_changed(self, sender: ImportsManager, libraries: List[LibraryDoc]) -> None:
541+
self.libraries_changed(self, libraries)
542+
543+
def _on_resources_changed(self, sender: ImportsManager, resources: List[LibraryDoc]) -> None:
544+
self.resources_changed(self, resources)
545+
546+
def _on_variables_changed(self, sender: ImportsManager, variables: List[LibraryDoc]) -> None:
547+
self.variables_changed(self, variables)
548+
513549
def default_imports_manager(self) -> ImportsManager:
514550
with self._imports_managers_lock:
515551
if self._default_imports_manager is None:

packages/robot/src/robotcode/robot/diagnostics/entities.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,10 @@ class VariableDefinition(SourceEntity):
195195
value: Any = field(default=None, compare=False)
196196
value_is_native: bool = field(default=False, compare=False)
197197

198-
@property
199-
def matcher(self) -> VariableMatcher:
200-
if not hasattr(self, "__matcher"):
201-
self.__matcher = VariableMatcher(self.name)
202-
return self.__matcher
198+
matcher: VariableMatcher = field(init=False, compare=False)
199+
200+
def __post_init__(self) -> None:
201+
self.matcher = VariableMatcher(self.name)
203202

204203
@single_call
205204
def __hash__(self) -> int:

packages/robot/src/robotcode/robot/diagnostics/imports_manager.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -503,8 +503,8 @@ def __init__(
503503
super().__init__()
504504

505505
self.documents_manager = documents_manager
506-
self.documents_manager.did_create_uri.add(self.possible_imports_modified)
507-
self.documents_manager.did_change.add(self.possible_resource_document_modified)
506+
self.documents_manager.did_create_uri.add(self._on_possible_imports_modified)
507+
self.documents_manager.did_change.add(self._on_possible_resource_document_modified)
508508

509509
self.file_watcher_manager: FileWatcherManagerBase = (
510510
file_watcher_manager if file_watcher_manager is not None else FileWatcherManagerDummy()
@@ -685,12 +685,15 @@ def variables_changed(sender, variables: List[LibraryDoc]) -> None: ...
685685
@event
686686
def imports_changed(sender, uri: DocumentUri) -> None: ...
687687

688-
def possible_imports_modified(self, sender: Any, uri: DocumentUri) -> None:
688+
def _on_possible_imports_modified(self, sender: Any, uri: DocumentUri) -> None:
689689
# TODO: do we really need this?
690690
self.imports_changed(self, uri)
691691

692692
@language_id("robotframework")
693-
def possible_resource_document_modified(self, sender: Any, document: TextDocument) -> None:
693+
def _on_possible_resource_document_modified(self, sender: Any, document: TextDocument) -> None:
694+
run_as_task(self.__on_possible_resource_document_modified, sender, document)
695+
696+
def __on_possible_resource_document_modified(self, sender: Any, document: TextDocument) -> None:
694697
with self._resource_document_changed_timer_lock:
695698
if document in self._resource_document_changed_documents:
696699
return
@@ -714,7 +717,7 @@ def __resource_documents_changed(self) -> None:
714717
self._resource_document_changed_documents = set()
715718

716719
for document in documents:
717-
run_as_task(self.__resource_document_changed, document).result()
720+
self.__resource_document_changed(document)
718721

719722
def __resource_document_changed(self, document: TextDocument) -> None:
720723
resource_changed: List[LibraryDoc] = []

0 commit comments

Comments
 (0)