Skip to content

Commit 9d70610

Browse files
committed
perf: Caching of variable imports
1 parent a7c3c0d commit 9d70610

File tree

13 files changed

+183
-37
lines changed

13 files changed

+183
-37
lines changed

package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,18 @@
493493
"type": "string"
494494
},
495495
"markdownDescription": [
496-
"Specifies the library names that should not be cached. This is useful if you have a dynamic or hybrid library that has different keywords depending on the arguments. You can specify a glob pattern that matches the library name or the source file. \n\nExamples:\n- `**/mylibfolder/mylib.py`\n- `MyLib`\n- `mylibfolder.mylib.subpackage` \n\nFor robot framework internal libraries, you have to specify the full module name like `robot.libraries.Remote`.\n\nIf you change this setting, you may need to run the command `Robot Code: Clear Cache and Restart Language Servers`."
496+
"Specifies the library names that should not be cached. This is useful if you have a dynamic or hybrid library that has different keywords depending on the arguments. You can specify a glob pattern that matches the library name or the source file. \n\nExamples:\n- `**/mylibfolder/mylib.py`\n- `MyLib`\n- `mylib.subpackage.subpackage` \n\nFor robot framework internal libraries, you have to specify the full module name like `robot.libraries.Remote`.\n\nIf you change this setting, you may need to run the command `Robot Code: Clear Cache and Restart Language Servers`."
497+
],
498+
"scope": "resource"
499+
},
500+
"robotcode.analysis.cache.ignoredVariables": {
501+
"type": "array",
502+
"default": [],
503+
"items": {
504+
"type": "string"
505+
},
506+
"markdownDescription": [
507+
"Specifies the variable files that should not be cached. This is useful if you have a dynamic or hybrid variable files that has different variables depending on the arguments. You can specify a glob pattern that matches the variable module name or the source file. \n\nExamples:\n- `**/variables/myvars.py`\n- `MyVariables`\n- `myvars.subpackage.subpackage` \n\nIf you change this setting, you may need to run the command `Robot Code: Clear Cache and Restart Language Servers`."
497508
],
498509
"scope": "resource"
499510
},

robotcode/debugger/launcher/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ async def _terminate(self, arguments: Optional[TerminateArguments] = None, *args
317317

318318
async def handle_unknown_command(self, message: Request) -> Any:
319319
if self.connected:
320-
self._logger.info("Forward request to client...")
320+
self._logger.debug("Forward request to client...")
321321

322322
return await self.client.protocol.send_request_async(message)
323323

robotcode/debugger/protocol.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def done(t: asyncio.Task[Any]) -> None:
248248
try:
249249
self.send_response(message.seq, message.command, t.result())
250250
except asyncio.CancelledError:
251-
self._logger.info(f"request message {repr(message)} canceled")
251+
self._logger.debug(f"request message {repr(message)} canceled")
252252
except (SystemExit, KeyboardInterrupt):
253253
raise
254254
except DebugAdapterRPCErrorException as ex:

robotcode/language_server/common/parts/commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def extend_capabilities(self, capabilities: ServerCapabilities) -> None:
6767
async def _workspace_execute_command(
6868
self, command: str, arguments: Optional[List[LSPAny]], *args: Any, **kwargs: Any
6969
) -> Optional[LSPAny]:
70-
self._logger.info(f"execute command {command}")
70+
self._logger.debug(f"execute command {command}")
7171

7272
entry = self.commands.get(command, None)
7373
if entry is None or entry.callback is None:

robotcode/language_server/robotframework/configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class WorkspaceConfig(ConfigBase):
8383
class Cache(ConfigBase):
8484
save_location: CacheSaveLocation = CacheSaveLocation.WORKSPACE_STORAGE
8585
ignored_libraries: List[str] = field(default_factory=list)
86+
ignored_variables: List[str] = field(default_factory=list)
8687

8788

8889
@config_section("robotcode.analysis")

robotcode/language_server/robotframework/diagnostics/entities.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class VariableDefinition(SourceEntity):
177177
resolvable: bool = field(default=False, compare=False)
178178

179179
value: Any = field(default=None, compare=False)
180+
value_is_native: bool = field(default=False, compare=False)
180181

181182
__matcher: Optional[VariableMatcher] = None
182183

@@ -250,9 +251,21 @@ def __hash__(self) -> int:
250251
return hash((type(self), self.name, self.type, self.range, self.source))
251252

252253

254+
@dataclass(frozen=True, eq=False, repr=False)
255+
class NativeValue:
256+
value: Any
257+
258+
def __repr__(self) -> str:
259+
return repr(self.value)
260+
261+
def __str__(self) -> str:
262+
return str(self.value)
263+
264+
253265
@dataclass
254266
class ImportedVariableDefinition(VariableDefinition):
255267
type: VariableDefinitionType = VariableDefinitionType.IMPORTED_VARIABLE
268+
value: Optional[NativeValue] = field(default=None, compare=False)
256269

257270
@single_call
258271
def __hash__(self) -> int:

robotcode/language_server/robotframework/diagnostics/imports_manager.py

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,17 @@ def __init__(self, parent_protocol: RobotLanguageServerProtocol, folder: Uri, co
519519
/ get_robot_version_str()
520520
/ "libdoc"
521521
)
522+
self.variables_doc_cache_path = (
523+
self.cache_path
524+
/ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
525+
/ get_robot_version_str()
526+
/ "variables"
527+
)
528+
522529
self.config = config
523530

524531
self.ignored_libraries_patters = [Pattern(s) for s in config.analysis.cache.ignored_libraries]
532+
self.ignored_variables_patters = [Pattern(s) for s in config.analysis.cache.ignored_variables]
525533
self._libaries_lock = Lock()
526534
self._libaries: OrderedDict[_LibrariesEntryKey, _LibrariesEntry] = OrderedDict()
527535
self._resources_lock = Lock()
@@ -796,9 +804,68 @@ async def get_library_meta(
796804
or (p.matches(result.origin) if result.origin is not None else False)
797805
for p in self.ignored_libraries_patters
798806
):
799-
self._logger.critical(
800-
lambda: f"Ignore library {result.name or ''} {result.origin or ''} for caching." # type: ignore
807+
self._logger.debug(f"Ignore library {result.name or ''} {result.origin or ''} for caching.")
808+
return None, import_name
809+
810+
if result.origin is not None:
811+
result.mtimes = {result.origin: Path(result.origin).resolve().stat().st_mtime_ns}
812+
813+
if result.submodule_search_locations:
814+
if result.mtimes is None:
815+
result.mtimes = {}
816+
result.mtimes.update(
817+
{
818+
str(f): f.resolve().stat().st_mtime_ns
819+
for f in itertools.chain(
820+
*(iter_files(loc, "**/*.py") for loc in result.submodule_search_locations)
821+
)
822+
}
801823
)
824+
825+
return result, import_name
826+
except (SystemExit, KeyboardInterrupt):
827+
raise
828+
except BaseException:
829+
pass
830+
831+
return None, import_name
832+
833+
async def get_variables_meta(
834+
self,
835+
name: str,
836+
base_dir: str = ".",
837+
variables: Optional[Dict[str, Optional[Any]]] = None,
838+
) -> Tuple[Optional[LibraryMetaData], str]:
839+
try:
840+
import_name = await self.find_variables(
841+
name,
842+
base_dir=base_dir,
843+
variables=variables,
844+
)
845+
846+
result: Optional[LibraryMetaData] = None
847+
module_spec: Optional[ModuleSpec] = None
848+
if is_variables_by_path(import_name):
849+
if (p := Path(import_name)).exists():
850+
result = LibraryMetaData(__version__, p.stem, import_name, None, True)
851+
else:
852+
module_spec = get_module_spec(import_name)
853+
if module_spec is not None and module_spec.origin is not None:
854+
result = LibraryMetaData(
855+
__version__,
856+
module_spec.name,
857+
module_spec.origin,
858+
module_spec.submodule_search_locations,
859+
False,
860+
)
861+
862+
if result is not None:
863+
if any(
864+
(p.matches(result.name) if result.name is not None else False)
865+
or (p.matches(result.origin) if result.origin is not None else False)
866+
for p in self.ignored_variables_patters
867+
):
868+
self._logger.debug(f"Ignore Variables {result.name or ''} {result.origin or ''} for caching.")
802869
return None, import_name
803870

804871
if result.origin is not None:
@@ -968,11 +1035,17 @@ async def _get_libdoc(name: str, args: Tuple[Any, ...], working_dir: str, base_d
9681035
try:
9691036
if meta is not None:
9701037
meta_file = Path(self.lib_doc_cache_path, meta.filepath_base.with_suffix(".meta.json"))
971-
meta_file.parent.mkdir(parents=True, exist_ok=True)
972-
meta_file.write_text(as_json(meta), "utf-8")
973-
9741038
spec_file = Path(self.lib_doc_cache_path, meta.filepath_base.with_suffix(".spec.json"))
975-
spec_file.write_text(as_json(result), "utf-8")
1039+
spec_file.parent.mkdir(parents=True, exist_ok=True)
1040+
1041+
try:
1042+
spec_file.write_text(as_json(result), "utf-8")
1043+
except (SystemExit, KeyboardInterrupt):
1044+
raise
1045+
except BaseException as e:
1046+
raise RuntimeError(f"Cannot write spec file for library '{name}' to '{spec_file}'") from e
1047+
1048+
meta_file.write_text(as_json(meta), "utf-8")
9761049
else:
9771050
self._logger.debug(lambda: f"Skip caching library {name}{repr(args)}")
9781051
except (SystemExit, KeyboardInterrupt):
@@ -1145,7 +1218,38 @@ async def get_libdoc_for_variables_import(
11451218
)
11461219

11471220
async def _get_libdoc() -> VariablesDoc:
1221+
meta, source = await self.get_variables_meta(
1222+
name,
1223+
base_dir,
1224+
variables,
1225+
)
1226+
11481227
self._logger.debug(lambda: f"Load variables {source}{repr(args)}")
1228+
if meta is not None:
1229+
meta_file = Path(self.variables_doc_cache_path, meta.filepath_base.with_suffix(".meta.json"))
1230+
if meta_file.exists():
1231+
try:
1232+
try:
1233+
saved_meta = from_json(meta_file.read_text("utf-8"), LibraryMetaData)
1234+
spec_path = None
1235+
if saved_meta == meta:
1236+
spec_path = Path(
1237+
self.variables_doc_cache_path, meta.filepath_base.with_suffix(".spec.json")
1238+
)
1239+
return from_json(
1240+
spec_path.read_text("utf-8"),
1241+
VariablesDoc,
1242+
)
1243+
except (SystemExit, KeyboardInterrupt):
1244+
raise
1245+
except BaseException as e:
1246+
raise RuntimeError(
1247+
f"Failed to load library meta data for library {name} from {spec_path}"
1248+
) from e
1249+
except (SystemExit, KeyboardInterrupt):
1250+
raise
1251+
except BaseException as e:
1252+
self._logger.exception(e)
11491253

11501254
with ProcessPoolExecutor(max_workers=1) as executor:
11511255
result = await asyncio.wait_for(
@@ -1166,6 +1270,26 @@ async def _get_libdoc() -> VariablesDoc:
11661270
self._logger.warning(
11671271
lambda: f"stdout captured at loading variables {name}{repr(args)}:\n{result.stdout}"
11681272
)
1273+
1274+
try:
1275+
if meta is not None:
1276+
meta_file = Path(self.variables_doc_cache_path, meta.filepath_base.with_suffix(".meta.json"))
1277+
spec_file = Path(self.variables_doc_cache_path, meta.filepath_base.with_suffix(".spec.json"))
1278+
spec_file.parent.mkdir(parents=True, exist_ok=True)
1279+
1280+
try:
1281+
spec_file.write_text(as_json(result), "utf-8")
1282+
except (SystemExit, KeyboardInterrupt):
1283+
raise
1284+
except BaseException as e:
1285+
raise RuntimeError(f"Cannot write spec file for variables '{name}' to '{spec_file}'") from e
1286+
meta_file.write_text(as_json(meta), "utf-8")
1287+
else:
1288+
self._logger.debug(lambda: f"Skip caching variables {name}{repr(args)}")
1289+
except (SystemExit, KeyboardInterrupt):
1290+
raise
1291+
except BaseException as e:
1292+
self._logger.exception(e)
11691293
return result
11701294

11711295
entry_key = _VariablesEntryKey(source, args)

robotcode/language_server/robotframework/diagnostics/library_doc.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
from .entities import (
3939
ArgumentDefinition,
4040
ImportedVariableDefinition,
41+
NativeValue,
4142
SourceEntity,
42-
VariableDefinition,
4343
single_call,
4444
)
4545

@@ -811,7 +811,7 @@ class VariablesDoc(LibraryDoc):
811811
type: str = "VARIABLES"
812812
scope: str = "GLOBAL"
813813

814-
variables: List[VariableDefinition] = field(default_factory=list)
814+
variables: List[ImportedVariableDefinition] = field(default_factory=list)
815815

816816

817817
def is_library_by_path(path: str) -> bool:
@@ -1488,20 +1488,6 @@ def find_variables(
14881488
)
14891489

14901490

1491-
# @dataclass
1492-
class NativeValue:
1493-
__slots__ = ["value"]
1494-
1495-
def __init__(self, value: Any) -> None:
1496-
self.value = value
1497-
1498-
def __repr__(self) -> str:
1499-
return repr(self.value)
1500-
1501-
def __str__(self) -> str:
1502-
return str(self.value)
1503-
1504-
15051491
def get_variables_doc(
15061492
name: str,
15071493
args: Optional[Tuple[Any, ...]] = None,
@@ -1554,7 +1540,7 @@ def import_variables(self, path: str, args: Optional[Tuple[Any, ...]] = None) ->
15541540

15551541
importer = MyPythonImporter(libcode)
15561542

1557-
vars: List[VariableDefinition] = [
1543+
vars: List[ImportedVariableDefinition] = [
15581544
ImportedVariableDefinition(
15591545
1,
15601546
0,
@@ -1564,9 +1550,10 @@ def import_variables(self, path: str, args: Optional[Tuple[Any, ...]] = None) ->
15641550
name,
15651551
None,
15661552
value=NativeValue(value)
1567-
if isinstance(value, (int, float, bool, str, set, tuple, dict, list))
1553+
if value is None or isinstance(value, (int, float, bool, str, dict, list))
15681554
else None,
1569-
has_value=isinstance(value, (int, float, bool, str, set, tuple, dict, list)),
1555+
has_value=value is None or isinstance(value, (int, float, bool, str, dict, list)),
1556+
value_is_native=value is None or isinstance(value, (int, float, bool, str, dict, list)),
15701557
)
15711558
for name, value in importer.import_variables(import_name, args)
15721559
]

robotcode/language_server/robotframework/diagnostics/namespace.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
cast,
2929
)
3030

31-
from ....utils.async_itertools import as_async_iterable
3231
from ....utils.async_tools import Lock, async_event
3332
from ....utils.logging import LoggingDescriptor
3433
from ....utils.uri import Uri
@@ -62,6 +61,7 @@
6261
CommandLineVariableDefinition,
6362
EnvironmentVariableDefinition,
6463
Import,
64+
ImportedVariableDefinition,
6565
InvalidVariableError,
6666
LibraryImport,
6767
LocalVariableDefinition,
@@ -537,7 +537,7 @@ class ResourceEntry(LibraryEntry):
537537

538538
@dataclass
539539
class VariablesEntry(LibraryEntry):
540-
variables: List[VariableDefinition] = field(default_factory=lambda: [])
540+
variables: List[ImportedVariableDefinition] = field(default_factory=lambda: [])
541541

542542

543543
class DocumentType(enum.Enum):
@@ -611,6 +611,8 @@ def __init__(
611611

612612
self._in_initialize = False
613613

614+
self._ignored_lines: Optional[List[int]] = None
615+
614616
@async_event
615617
async def has_invalidated(sender) -> None: # NOSONAR
616618
...
@@ -689,6 +691,7 @@ async def _invalidate(self) -> None:
689691

690692
self._finder = None
691693
self._in_initialize = False
694+
self._ignored_lines = None
692695

693696
await self._reset_global_variables()
694697

@@ -1192,7 +1195,7 @@ async def _import(
11921195
self._logger.debug(lambda: f"start imports for {self.document if top_level else source}")
11931196
try:
11941197

1195-
async for imp in as_async_iterable(imports):
1198+
for imp in imports:
11961199
if variables is None:
11971200
variables = await self.get_resolvable_variables()
11981201

@@ -1552,7 +1555,7 @@ async def append_diagnostics(
15521555
related_information: Optional[List[DiagnosticRelatedInformation]] = None,
15531556
data: Optional[Any] = None,
15541557
) -> None:
1555-
if await self.should_ignore(self.document, range):
1558+
if await self._should_ignore(range):
15561559
return
15571560

15581561
self._diagnostics.append(
@@ -1667,6 +1670,12 @@ async def __get_ignored_lines(document: TextDocument) -> List[int]:
16671670
async def should_ignore(cls, document: Optional[TextDocument], range: Range) -> bool:
16681671
return cls.__should_ignore(await cls.get_ignored_lines(document) if document is not None else [], range)
16691672

1673+
async def _should_ignore(self, range: Range) -> bool:
1674+
if self._ignored_lines is None:
1675+
self._ignored_lines = await self.get_ignored_lines(self.document) if self.document is not None else []
1676+
1677+
return self.__should_ignore(self._ignored_lines, range)
1678+
16701679
@staticmethod
16711680
def __should_ignore(lines: List[int], range: Range) -> bool:
16721681
import builtins

0 commit comments

Comments
 (0)