From 77db5024fb7a5fa56fb895e5a3d9a371910c7bf5 Mon Sep 17 00:00:00 2001 From: Daniel Biehl Date: Thu, 4 Jan 2024 13:47:32 +0100 Subject: [PATCH] fix(langserver): preventing extensive calls to 'workspace/configuration' through caching --- .../language_server/common/parts/workspace.py | 48 +++++++++---------- .../robotframework/parts/references.py | 43 ++++++++++------- .../robotframework/parts/conftest.py | 5 +- vscode-client/extension.ts | 2 + 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/packages/language_server/src/robotcode/language_server/common/parts/workspace.py b/packages/language_server/src/robotcode/language_server/common/parts/workspace.py index ce05df1a9..da805609d 100644 --- a/packages/language_server/src/robotcode/language_server/common/parts/workspace.py +++ b/packages/language_server/src/robotcode/language_server/common/parts/workspace.py @@ -1,15 +1,12 @@ -from __future__ import annotations - -import asyncio import threading import uuid import weakref from concurrent.futures import Future -from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, Callable, + ClassVar, Coroutine, Dict, Final, @@ -17,13 +14,11 @@ Mapping, NamedTuple, Optional, - Protocol, Tuple, Type, TypeVar, Union, cast, - runtime_checkable, ) from robotcode.core.concurrent import threaded @@ -125,6 +120,9 @@ def __init__(self, name: str, uri: Uri, document_uri: DocumentUri) -> None: self.document_uri = document_uri +_F = TypeVar("_F", bound=Callable[..., Any]) + + def config_section(name: str) -> Callable[[_F], _F]: def decorator(func: _F) -> _F: setattr(func, "__config_section__", name) @@ -133,18 +131,12 @@ def decorator(func: _F) -> _F: return decorator -@runtime_checkable -class HasConfigSection(Protocol): - __config_section__: str - - -@dataclass +# @dataclass class ConfigBase(CamelSnakeMixin): - pass + __config_section__: ClassVar[str] _TConfig = TypeVar("_TConfig", bound=ConfigBase) -_F = TypeVar("_F", bound=Callable[..., Any]) class Workspace(LanguageServerProtocolPart): @@ -152,7 +144,7 @@ class Workspace(LanguageServerProtocolPart): def __init__( self, - parent: LanguageServerProtocol, + parent: "LanguageServerProtocol", root_uri: Optional[str], root_path: Optional[str], workspace_folders: Optional[List[TypesWorkspaceFolder]] = None, @@ -174,6 +166,7 @@ def __init__( self.parent.on_shutdown.add(self.server_shutdown) self.parent.on_initialize.add(self.server_initialize) + self._settings_cache: Dict[Tuple[Optional[WorkspaceFolder], str], ConfigBase] = {} def server_initialize(self, sender: Any, initialization_options: Optional[Any] = None) -> None: if ( @@ -260,6 +253,7 @@ def did_change_configuration(sender, settings: Dict[str, Any]) -> None: # NOSON @threaded def _workspace_did_change_configuration(self, settings: Dict[str, Any], *args: Any, **kwargs: Any) -> None: self.settings = settings + self._settings_cache.clear() self.did_change_configuration(self, settings) @event @@ -333,14 +327,6 @@ def _workspace_will_delete_files(self, files: List[FileDelete], *args: Any, **kw def _workspace_did_delete_files(self, files: List[FileDelete], *args: Any, **kwargs: Any) -> None: self.did_delete_files(self, [f.uri for f in files]) - def get_configuration_async( - self, - section: Type[_TConfig], - scope_uri: Union[str, Uri, None] = None, - request: bool = True, - ) -> asyncio.Future[_TConfig]: - return asyncio.wrap_future(self.get_configuration_future(section, scope_uri, request)) - def get_configuration( self, section: Type[_TConfig], @@ -357,6 +343,12 @@ def get_configuration_future( ) -> Future[_TConfig]: result_future: Future[_TConfig] = Future() + scope = self.get_workspace_folder(scope_uri) if scope_uri is not None else None + + if (scope, section.__config_section__) in self._settings_cache: + result_future.set_result(cast(_TConfig, self._settings_cache[(scope, section.__config_section__)])) + return result_future + def _get_configuration_done(f: Future[Optional[Any]]) -> None: try: if result_future.cancelled(): @@ -371,12 +363,14 @@ def _get_configuration_done(f: Future[Optional[Any]]) -> None: return result = f.result() - result_future.set_result(from_dict(result[0] if result else {}, section)) + r = from_dict(result[0] if result else {}, section) + self._settings_cache[(scope, section.__config_section__)] = r + result_future.set_result(r) except Exception as e: result_future.set_exception(e) self.get_configuration_raw( - section=cast(HasConfigSection, section).__config_section__, + section=section.__config_section__, scope_uri=scope_uri, request=request, ).add_done_callback(_get_configuration_done) @@ -453,6 +447,10 @@ def _workspace_did_change_workspace_folders( for r in to_remove: self._workspace_folders.remove(r) + settings_to_remove = [k for k in self._settings_cache.keys() if k[0] == r] + for k in settings_to_remove: + self._settings_cache.pop(k, None) + for a in event.added: self._workspace_folders.append(WorkspaceFolder(a.name, Uri(a.uri), a.uri)) diff --git a/packages/language_server/src/robotcode/language_server/robotframework/parts/references.py b/packages/language_server/src/robotcode/language_server/robotframework/parts/references.py index 90b9ddb79..8f2b62275 100644 --- a/packages/language_server/src/robotcode/language_server/robotframework/parts/references.py +++ b/packages/language_server/src/robotcode/language_server/robotframework/parts/references.py @@ -207,28 +207,35 @@ def _find_variable_references( def find_variable_references_in_file( self, doc: TextDocument, variable: VariableDefinition, include_declaration: bool = True ) -> List[Location]: - namespace = self.parent.documents_cache.get_namespace(doc) + try: + namespace = self.parent.documents_cache.get_namespace(doc) - if ( - variable.source - and variable.source != str(doc.uri.to_path()) - and not any(e for e in (namespace.get_resources()).values() if e.library_doc.source == variable.source) - and not any( - e for e in namespace.get_imported_variables().values() if e.library_doc.source == variable.source - ) - and not any(e for e in namespace.get_command_line_variables() if e.source == variable.source) - ): - return [] + if ( + variable.source + and variable.source != str(doc.uri.to_path()) + and not any(e for e in (namespace.get_resources()).values() if e.library_doc.source == variable.source) + and not any( + e for e in namespace.get_imported_variables().values() if e.library_doc.source == variable.source + ) + and not any(e for e in namespace.get_command_line_variables() if e.source == variable.source) + ): + return [] - result = set() - if include_declaration and variable.source: - result.add(Location(str(Uri.from_path(variable.source)), variable.name_range)) + result = set() + if include_declaration and variable.source: + result.add(Location(str(Uri.from_path(variable.source)), variable.name_range)) - refs = namespace.get_variable_references() - if variable in refs: - result |= refs[variable] + refs = namespace.get_variable_references() + if variable in refs: + result |= refs[variable] - return list(result) + return list(result) + except (SystemExit, KeyboardInterrupt, CancelledError): + raise + except BaseException as e: + self._logger.exception(e) + + return [] @_logger.call def find_keyword_references_in_file( diff --git a/tests/robotcode/language_server/robotframework/parts/conftest.py b/tests/robotcode/language_server/robotframework/parts/conftest.py index a1cae72e7..de3b32029 100644 --- a/tests/robotcode/language_server/robotframework/parts/conftest.py +++ b/tests/robotcode/language_server/robotframework/parts/conftest.py @@ -2,7 +2,7 @@ import dataclasses import shutil from pathlib import Path -from typing import AsyncIterator, Iterator, cast +from typing import AsyncIterator, Iterator import pytest import pytest_asyncio @@ -18,7 +18,6 @@ ) from robotcode.core.utils.dataclasses import as_dict from robotcode.language_server.common.parts.diagnostics import DiagnosticsMode -from robotcode.language_server.common.parts.workspace import HasConfigSection from robotcode.language_server.common.text_document import TextDocument from robotcode.language_server.robotframework.configuration import AnalysisConfig, RobotCodeConfig, RobotConfig from robotcode.language_server.robotframework.protocol import ( @@ -73,7 +72,7 @@ async def protocol(request: pytest.FixtureRequest) -> AsyncIterator[RobotLanguag ) protocol.workspace.settings = { - cast(HasConfigSection, RobotCodeConfig).__config_section__: as_dict( + RobotCodeConfig.__config_section__: as_dict( RobotCodeConfig( robot=RobotConfig( python_path=["./lib", "./resources"], diff --git a/vscode-client/extension.ts b/vscode-client/extension.ts index eb3adb3b1..491cc4ddf 100644 --- a/vscode-client/extension.ts +++ b/vscode-client/extension.ts @@ -126,6 +126,8 @@ export async function activateAsync(context: vscode.ExtensionContext): Promise