Skip to content

Commit d88f9bf

Browse files
authored
refactor: Do not load and store cache when cache is not enabled (#1607)
* refactor(caching): Do not load and store cache when cache is not enabled Reduce amount of cache reads if it's not needed. Split run() method into smaller methods. Implemented hash method inside config class. * test: re-run performance test
1 parent 6755d96 commit d88f9bf

File tree

4 files changed

+66
-47
lines changed

4 files changed

+66
-47
lines changed

src/robocop/cache.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def _is_entry_valid(
368368
True if the cache entry is valid, False otherwise.
369369
370370
"""
371-
# Check cheapest condition first (string comparison)
371+
# Check the cheapest condition first (string comparison)
372372
if config_hash != entry_config_hash:
373373
return False
374374

@@ -394,7 +394,7 @@ def _get_entry(
394394
Args:
395395
cache_dict: Dictionary containing cache entries (linter or formatter).
396396
path: Absolute path to the file.
397-
config_hash: Hash of current configuration.
397+
config_hash: Hash of the current configuration.
398398
399399
Returns:
400400
Cached entry if valid, None otherwise.
@@ -470,6 +470,8 @@ def set_linter_entry(
470470
diagnostics: List of diagnostics found.
471471
472472
"""
473+
if not self.enabled:
474+
return
473475
try:
474476
metadata = FileMetadata.from_path(path)
475477
except OSError:

src/robocop/config.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ class Config:
713713
verbose: bool | None = field(default_factory=bool)
714714
silent: bool | None = field(default_factory=bool)
715715
target_version: int | str | None = misc.ROBOT_VERSION.major
716+
_hash: int | None = None
716717
config_source: str = "cli"
717718

718719
def __post_init__(self) -> None:
@@ -863,6 +864,27 @@ def overwrite_from_config(self, overwrite_config: Config | None) -> None:
863864
def __str__(self):
864865
return str(self.config_source)
865866

867+
def hash(self) -> str:
868+
"""
869+
Compute cache key combining linter config hash with language.
870+
871+
Uses SHA256 for stable hashing across Python processes, unlike the built-in
872+
hash() which can vary due to hash randomization (PEP 456).
873+
874+
Returns:
875+
str: The computed cache key as a hexadecimal digest.
876+
877+
"""
878+
if self._hash is None:
879+
hasher = hashlib.sha256()
880+
# Hash the linter config
881+
hasher.update(str(hash(self.linter)).encode("utf-8"))
882+
# Hash the language configuration (affects parsing)
883+
language_str = ":".join(sorted(self.language or []))
884+
hasher.update(language_str.encode("utf-8"))
885+
self._hash = hasher.hexdigest()
886+
return self._hash
887+
866888

867889
class GitIgnoreResolver:
868890
def __init__(self):

src/robocop/linter/runner.py

Lines changed: 37 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import hashlib
43
from typing import TYPE_CHECKING, NoReturn
54

65
import typer
@@ -42,25 +41,39 @@ def get_model_for_file_type(self, source: Path, language: list[str] | None) -> F
4241
return get_resource_with_lang(get_resource_model, source, language)
4342
return get_resource_with_lang(get_model, source, language)
4443

45-
@staticmethod
46-
def _compute_cache_key(config: Config) -> str:
44+
def get_cached_diagnostics(self, config: Config, source: Path) -> list[Diagnostic] | None:
4745
"""
48-
Compute cache key combining linter config hash with language.
46+
Return cached diagnostics if available.
4947
50-
Uses SHA256 for stable hashing across Python processes, unlike the built-in
51-
hash() which can vary due to hash randomization (PEP 456).
48+
Returns:
49+
List of cached diagnostics or None if no cache is available.
50+
51+
"""
52+
if not config.cache.enabled:
53+
return None
54+
cached_entry = self.config_manager.cache.get_linter_entry(source, config.hash())
55+
56+
if cached_entry is not None:
57+
restored = restore_diagnostics(cached_entry, source, config.linter.rules)
58+
if restored is not None:
59+
return restored
60+
return None
61+
62+
def get_model_diagnostics(self, config: Config, source: Path) -> list[Diagnostic] | None:
63+
"""
64+
Run all selected rules on the model and return list of diagnostics.
5265
5366
Returns:
54-
str: The computed cache key as a hexadecimal digest.
67+
List of diagnostics or None if file cannot be decoded.
5568
5669
"""
57-
hasher = hashlib.sha256()
58-
# Hash the linter config
59-
hasher.update(str(hash(config.linter)).encode("utf-8"))
60-
# Hash the language configuration (affects parsing)
61-
language_str = ":".join(sorted(config.language or []))
62-
hasher.update(language_str.encode("utf-8"))
63-
return hasher.hexdigest()
70+
try:
71+
model = self.get_model_for_file_type(source, config.language)
72+
return self.run_check(model, source, config)
73+
except DataError as error:
74+
if not config.silent:
75+
print(f"Failed to decode {source} with an error: {error}. Skipping file")
76+
return None
6477

6578
def run(self) -> list[Diagnostic]:
6679
"""
@@ -89,36 +102,18 @@ def run(self) -> list[Diagnostic]:
89102
for source, config in self.config_manager.paths:
90103
if config.verbose:
91104
print(f"Scanning file: {source}")
92-
93-
# Try to get cached diagnostics
94-
config_hash = self._compute_cache_key(config)
95-
cached_entry = self.config_manager.cache.get_linter_entry(source, config_hash)
96-
97-
if cached_entry is not None:
98-
# Restore diagnostics from cache
99-
restored = restore_diagnostics(cached_entry, source, config.linter.rules)
100-
if restored is not None:
101-
self.diagnostics.extend(restored)
102-
files += 1
103-
cached_files += 1
104-
continue
105-
# If restoration failed (e.g., rule removed), fall through to reprocess
106-
107-
try:
108-
model = self.get_model_for_file_type(source, config.language)
109-
except DataError as error:
110-
if not config.silent:
111-
print(f"Failed to decode {source} with an error: {error}. Skipping file")
105+
diagnostics = self.get_cached_diagnostics(config, source)
106+
if diagnostics is not None:
107+
self.diagnostics.extend(diagnostics)
108+
files += 1
109+
cached_files += 1
110+
continue
111+
diagnostics = self.get_model_diagnostics(config, source)
112+
if diagnostics is None:
112113
continue
113-
114-
files += 1
115-
diagnostics = self.run_check(model, source, config)
116114
self.diagnostics.extend(diagnostics)
117-
118-
# Cache the results (config_hash already includes language)
119-
self.config_manager.cache.set_linter_entry(source, config_hash, diagnostics)
120-
121-
# Save cache at the end
115+
files += 1
116+
self.config_manager.cache.set_linter_entry(source, config.hash(), diagnostics)
122117
self.config_manager.cache.save()
123118

124119
if not files and not self.config_manager.default_config.silent:

tests/performance/reports/robocop_7_1_0.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
{
22
"linter_report": {
33
"with_print_cache": {
4-
"avg_time": 2.3083314374962356,
4+
"avg_time": 2.306937637476949,
55
"counter": 388
66
},
77
"with_print_no_cache": {
8-
"avg_time": 3.451982950005913,
8+
"avg_time": 3.4038116500305478,
99
"counter": 388
1010
},
1111
"without_print_cache": {
1212
"avg_time": 0.856108075066004,
1313
"counter": 388
1414
},
1515
"without_print_no_cache": {
16-
"avg_time": 1.9954930499952752,
16+
"avg_time": 1.946089800039772,
1717
"counter": 388
1818
}
1919
},

0 commit comments

Comments
 (0)