Skip to content

Commit 9b89623

Browse files
committed
perf(robot): cache LibraryDoc for robot and resource files on disk
Resource and robot files are now cached on disk between sessions. On subsequent opens, imports, keywords, and variables from these files are loaded from cache instead of being re-parsed, resulting in faster startup and quicker response times when navigating projects with many resource files. The cache is automatically invalidated when a file is modified on disk. Files that are currently open in the editor always use the live content and bypass the disk cache.
1 parent 45c410f commit 9b89623

File tree

4 files changed

+295
-199
lines changed

4 files changed

+295
-199
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
class CacheSection(Enum):
1313
LIBRARY = "libdoc"
1414
VARIABLES = "variables"
15+
RESOURCE = "resource"
1516

1617

1718
class DataCache(ABC):

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

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
CompleteResult,
6565
LibraryDoc,
6666
ModuleSpec,
67+
ResourceDoc,
6768
VariablesDoc,
6869
complete_library_import,
6970
complete_resource_import,
@@ -307,7 +308,7 @@ def __init__(
307308
self.name = name
308309
self.source_path = source_path
309310
self._document: Optional[TextDocument] = None
310-
self._lib_doc: Optional[LibraryDoc] = None
311+
self._lib_doc: Optional[ResourceDoc] = None
311312

312313
def __repr__(self) -> str:
313314
return f"{type(self).__qualname__}(name={self.name!r}, file_watchers={self.file_watchers!r}, id={id(self)!r}"
@@ -392,13 +393,16 @@ def get_namespace(self) -> "Namespace":
392393
def _get_namespace(self) -> "Namespace":
393394
return self.parent.get_namespace_for_resource(self._get_document())
394395

395-
def get_libdoc(self) -> LibraryDoc:
396+
def get_resource_doc(self) -> ResourceDoc:
396397
with self._lock:
397398
if self._lib_doc is None:
398-
self._lib_doc = self._get_namespace().get_library_doc()
399+
self._lib_doc = self.parent.get_resource_doc_from_document(self._get_document())
399400

400401
return self._lib_doc
401402

403+
def get_libdoc(self) -> ResourceDoc:
404+
return self.get_resource_doc()
405+
402406

403407
@dataclass(frozen=True, slots=True)
404408
class _VariablesEntryKey:
@@ -525,6 +529,18 @@ def filepath_base(self) -> str:
525529
raise ValueError("Cannot determine filepath base.")
526530

527531

532+
@dataclass
533+
class RobotFileMeta:
534+
meta_version: str
535+
source: str
536+
mtime_ns: int
537+
538+
@property
539+
def filepath_base(self) -> str:
540+
p = Path(self.source)
541+
return f"{zlib.adler32(str(p.parent).encode('utf-8')):08x}_{p.stem}{p.suffix}"
542+
543+
528544
class ImportsManager:
529545
_logger = LoggingDescriptor()
530546

@@ -613,7 +629,7 @@ def __init__(
613629
self._resource_document_changed_timer_interval = 1
614630
self._resource_document_changed_documents: Set[TextDocument] = set()
615631

616-
self._resource_libdoc_cache: "weakref.WeakKeyDictionary[ast.AST, Dict[str, LibraryDoc]]" = (
632+
self._resource_libdoc_cache: "weakref.WeakKeyDictionary[ast.AST, Dict[str, ResourceDoc]]" = (
617633
weakref.WeakKeyDictionary()
618634
)
619635

@@ -676,6 +692,17 @@ def environment(self) -> Mapping[str, str]:
676692
def get_namespace_for_resource(self, document: TextDocument) -> "Namespace":
677693
return self.document_cache_helper.get_resource_namespace(document)
678694

695+
def get_resource_doc_from_document(self, document: TextDocument) -> ResourceDoc:
696+
source = str(document.uri.to_path())
697+
698+
if not self._is_document_loaded(source):
699+
cached = self._get_model_doc_cached(source)
700+
if cached is not None:
701+
return cached
702+
703+
model = self.document_cache_helper.get_resource_model(document)
704+
return self.get_libdoc_from_model(model, source)
705+
679706
def clear_cache(self) -> None:
680707
if self.cache_path.exists():
681708
shutil.rmtree(self.cache_path, ignore_errors=True)
@@ -1407,17 +1434,29 @@ def get_libdoc_for_library_import(
14071434

14081435
return entry.get_libdoc()
14091436

1437+
def _is_document_loaded(self, source: str) -> bool:
1438+
doc = self.documents_manager.get(Uri.from_path(source))
1439+
return doc is not None and doc.version is not None
1440+
14101441
@_logger.call
14111442
def get_libdoc_from_model(
14121443
self,
14131444
model: ast.AST,
14141445
source: str,
1415-
) -> LibraryDoc:
1446+
) -> ResourceDoc:
1447+
14161448
entry = self._resource_libdoc_cache.get(model)
14171449
if entry is not None and source in entry:
14181450
return entry[source]
14191451

1420-
result = get_model_doc(model=model, source=source)
1452+
use_disk_cache = not self._is_document_loaded(source)
1453+
1454+
result = self._get_model_doc_cached(source) if use_disk_cache else None
1455+
if result is None:
1456+
result = get_model_doc(model=model, source=source)
1457+
if use_disk_cache:
1458+
self._save_model_doc_cache(source, result)
1459+
14211460
if entry is None:
14221461
entry = {}
14231462
self._resource_libdoc_cache[model] = entry
@@ -1426,6 +1465,62 @@ def get_libdoc_from_model(
14261465

14271466
return result
14281467

1468+
@staticmethod
1469+
def get_resource_meta(source: str) -> Optional[RobotFileMeta]:
1470+
try:
1471+
source_path = normalized_path(source)
1472+
if source_path.exists():
1473+
return RobotFileMeta(
1474+
__version__,
1475+
str(source_path),
1476+
os.stat(source_path, follow_symlinks=False).st_mtime_ns,
1477+
)
1478+
except OSError:
1479+
pass
1480+
return None
1481+
1482+
def _get_model_doc_cached(self, source: str) -> Optional[ResourceDoc]:
1483+
meta = self.get_resource_meta(source)
1484+
if meta is None:
1485+
return None
1486+
1487+
meta_file = meta.filepath_base + ".meta"
1488+
if not self.data_cache.cache_data_exists(CacheSection.RESOURCE, meta_file):
1489+
return None
1490+
1491+
try:
1492+
saved_meta = self.data_cache.read_cache_data(CacheSection.RESOURCE, meta_file, RobotFileMeta)
1493+
if saved_meta == meta:
1494+
spec_file = meta.filepath_base + ".spec"
1495+
return self.data_cache.read_cache_data(CacheSection.RESOURCE, spec_file, ResourceDoc)
1496+
except (SystemExit, KeyboardInterrupt):
1497+
raise
1498+
except BaseException as e:
1499+
ex = e
1500+
self._logger.debug(
1501+
lambda: f"Failed to load cached model doc for {source}: {ex}",
1502+
context_name="import",
1503+
)
1504+
1505+
return None
1506+
1507+
def _save_model_doc_cache(self, source: str, result: ResourceDoc) -> None:
1508+
meta = self.get_resource_meta(source)
1509+
if meta is None:
1510+
return
1511+
1512+
try:
1513+
self.data_cache.save_cache_data(CacheSection.RESOURCE, meta.filepath_base + ".spec", result)
1514+
self.data_cache.save_cache_data(CacheSection.RESOURCE, meta.filepath_base + ".meta", meta)
1515+
except (SystemExit, KeyboardInterrupt):
1516+
raise
1517+
except BaseException as e:
1518+
ex = e
1519+
self._logger.debug(
1520+
lambda: f"Failed to save model doc cache for {source}: {ex}",
1521+
context_name="import",
1522+
)
1523+
14291524
def _get_variables_libdoc(
14301525
self,
14311526
name: str,
@@ -1629,6 +1724,20 @@ def get_namespace_for_resource_import(
16291724

16301725
return entry.get_namespace()
16311726

1727+
def get_resource_doc_for_resource_import(
1728+
self,
1729+
name: str,
1730+
base_dir: str,
1731+
sentinel: Any = None,
1732+
variables: Optional[Dict[str, Any]] = None,
1733+
*,
1734+
source: Optional[str] = None,
1735+
) -> ResourceDoc:
1736+
with self._logger.measure_time(lambda: f"getting resource doc for {name}", context_name="import"):
1737+
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables, source=source)
1738+
1739+
return entry.get_resource_doc()
1740+
16321741
def get_libdoc_for_resource_import(
16331742
self,
16341743
name: str,
@@ -1637,10 +1746,10 @@ def get_libdoc_for_resource_import(
16371746
variables: Optional[Dict[str, Any]] = None,
16381747
*,
16391748
source: Optional[str] = None,
1640-
) -> LibraryDoc:
1749+
) -> ResourceDoc:
16411750
entry = self._get_entry_for_resource_import(name, base_dir, sentinel, variables, source=source)
16421751

1643-
return entry.get_libdoc()
1752+
return entry.get_resource_doc()
16441753

16451754
def complete_library_import(
16461755
self,

0 commit comments

Comments
 (0)