Skip to content

Commit abf3387

Browse files
committed
feat(robot): add namespace disk cache for cold-start acceleration
Speed up first-time file analysis by caching fully resolved namespace results to disk. On subsequent IDE starts, cached namespaces are reused instead of re-analyzing every file from scratch, significantly reducing the time until diagnostics, code completion, and navigation become available. The cache is automatically invalidated when source files, library dependencies, environment variables, command-line variables, language configuration, or the RobotCode version change.
1 parent 5734690 commit abf3387

File tree

6 files changed

+1845
-7
lines changed

6 files changed

+1845
-7
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
@@ -13,6 +13,7 @@ class CacheSection(Enum):
1313
LIBRARY = "libdoc"
1414
VARIABLES = "variables"
1515
RESOURCE = "resource"
16+
NAMESPACE = "namespace"
1617

1718

1819
class DataCache(ABC):

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

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,15 @@
3434
from ..config.model import RobotBaseProfile
3535
from ..utils import get_robot_version
3636
from ..utils.stubs import Languages
37-
from .imports_manager import ImportsManager
37+
from .data_cache import CacheSection
38+
from .imports_manager import ImportsManager, NamespaceMetaData
3839
from .library_doc import LibraryDoc
39-
from .namespace import DocumentType, Namespace, NamespaceBuilder
40+
from .namespace import (
41+
DocumentType,
42+
Namespace,
43+
NamespaceBuilder,
44+
NamespaceData,
45+
)
4046
from .project_index import ProjectIndex
4147
from .workspace_config import (
4248
AnalysisDiagnosticModifiersConfig,
@@ -409,6 +415,16 @@ def get_only_initialized_namespace(self, document: TextDocument) -> Optional[Nam
409415
def __get_namespace_for_document_type(
410416
self, document: TextDocument, document_type: Optional[DocumentType]
411417
) -> Namespace:
418+
source = str(document.uri.to_path())
419+
imports_manager = self.get_imports_manager(document)
420+
421+
# --- Try disk cache (cold-start acceleration) ---
422+
if document.version is None:
423+
result = self._try_load_cached_namespace(source, document, document_type, imports_manager)
424+
if result is not None:
425+
return result
426+
427+
# --- Cache miss: full build ---
412428
if document_type is not None and document_type == DocumentType.INIT:
413429
model = self.get_init_model(document)
414430
elif document_type is not None and document_type == DocumentType.RESOURCE:
@@ -418,14 +434,12 @@ def __get_namespace_for_document_type(
418434
else:
419435
model = self.get_model(document)
420436

421-
imports_manager = self.get_imports_manager(document)
422-
423437
languages, workspace_languages = self.build_languages_from_model(document, model)
424438

425439
builder = NamespaceBuilder(
426440
imports_manager,
427441
model,
428-
str(document.uri.to_path()),
442+
source,
429443
document,
430444
document_type,
431445
languages,
@@ -434,6 +448,9 @@ def __get_namespace_for_document_type(
434448

435449
result = builder.build()
436450

451+
# Save to disk cache
452+
self._save_namespace_to_cache(source, result, imports_manager)
453+
437454
# Update the folder-scoped reference index
438455
self.get_project_index(document).update_file(result.source, result)
439456

@@ -446,6 +463,119 @@ def __get_namespace_for_document_type(
446463

447464
return result
448465

466+
def _try_load_cached_namespace(
467+
self,
468+
source: str,
469+
document: TextDocument,
470+
document_type: Optional[DocumentType],
471+
imports_manager: ImportsManager,
472+
) -> Optional[Namespace]:
473+
"""Attempt to load a Namespace from the disk cache.
474+
475+
Returns None on cache miss or validation failure.
476+
"""
477+
data_cache = imports_manager.data_cache
478+
479+
# Check source file exists before attempting cache lookup
480+
if not Path(source).exists():
481+
return None
482+
483+
# Compute filepath_base from source path
484+
temp_filepath_base = NamespaceMetaData(
485+
meta_version="",
486+
source=source,
487+
source_mtime_ns=0,
488+
config_fingerprint="",
489+
).filepath_base
490+
491+
meta_file = temp_filepath_base + ".meta"
492+
if not data_cache.cache_data_exists(CacheSection.NAMESPACE, meta_file):
493+
return None
494+
495+
try:
496+
saved_meta = data_cache.read_cache_data(CacheSection.NAMESPACE, meta_file, NamespaceMetaData)
497+
except (SystemExit, KeyboardInterrupt):
498+
raise
499+
except BaseException as e:
500+
ex = e
501+
self._logger.debug(
502+
lambda: f"Failed to read namespace meta for {source}: {ex}",
503+
context_name="import",
504+
)
505+
return None
506+
507+
if not imports_manager.validate_namespace_meta(saved_meta):
508+
return None
509+
510+
# Meta is valid — load the full NamespaceData
511+
data_file = temp_filepath_base + ".data"
512+
try:
513+
namespace_data = data_cache.read_cache_data(CacheSection.NAMESPACE, data_file, NamespaceData)
514+
except (SystemExit, KeyboardInterrupt):
515+
raise
516+
except BaseException as e:
517+
ex = e
518+
self._logger.debug(
519+
lambda: f"Failed to read namespace data for {source}: {ex}",
520+
context_name="import",
521+
)
522+
return None
523+
524+
# Reconstruct the Namespace from cached data.
525+
# Try to get the file's ResourceDoc from the RESOURCE disk cache first
526+
# (avoids parsing the model if already cached). Falls back to parsing.
527+
try:
528+
library_doc = imports_manager.get_resource_doc_from_document(document)
529+
result = Namespace.from_data(namespace_data, imports_manager, library_doc, document)
530+
except (SystemExit, KeyboardInterrupt):
531+
raise
532+
except BaseException as e:
533+
ex = e
534+
self._logger.debug(
535+
lambda: f"Failed to reconstruct namespace from cache for {source}: {ex}",
536+
context_name="import",
537+
)
538+
return None
539+
540+
self._logger.debug(
541+
lambda: f"Loaded namespace from disk cache for {source}",
542+
context_name="import",
543+
)
544+
545+
# Update the folder-scoped reference index
546+
self.get_project_index(document).update_file(result.source, result)
547+
548+
result.invalidated.add(self._invalidate_namespace)
549+
self.__namespace_initialized(result)
550+
551+
return result
552+
553+
def _save_namespace_to_cache(
554+
self,
555+
source: str,
556+
namespace: Namespace,
557+
imports_manager: ImportsManager,
558+
) -> None:
559+
"""Save a Namespace to the disk cache."""
560+
try:
561+
meta = imports_manager.build_namespace_meta(source, namespace)
562+
data = namespace.to_data()
563+
564+
data_cache = imports_manager.data_cache
565+
data_file = meta.filepath_base + ".data"
566+
meta_file = meta.filepath_base + ".meta"
567+
568+
data_cache.save_cache_data(CacheSection.NAMESPACE, data_file, data)
569+
data_cache.save_cache_data(CacheSection.NAMESPACE, meta_file, meta)
570+
except (SystemExit, KeyboardInterrupt):
571+
raise
572+
except BaseException as e:
573+
ex = e
574+
self._logger.debug(
575+
lambda: f"Failed to save namespace cache for {source}: {ex}",
576+
context_name="import",
577+
)
578+
449579
def create_imports_manager(self, root_uri: Uri) -> ImportsManager:
450580
cache_base_path = self.calc_cache_path(root_uri)
451581

0 commit comments

Comments
 (0)