Skip to content

Commit 795c420

Browse files
committed
perf(cache): defer data blob loading in CacheEntry until first access
1 parent eeec613 commit 795c420

File tree

2 files changed

+43
-11
lines changed

2 files changed

+43
-11
lines changed

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,26 @@ class CacheSection(Enum):
1616

1717

1818
class CacheEntry(Generic[_M, _D]):
19-
"""Lazy-deserializing cache entry.
19+
"""Lazy cache entry that defers both deserialization and data blob loading.
2020
21-
Meta and data blobs are deserialized on first property access, not when read from DB.
21+
Only the meta blob is read from the DB initially. The data blob is fetched
22+
lazily on first `.data` access, avoiding the transfer of large blobs when
23+
only meta validation is needed (e.g. on cache misses).
2224
"""
2325

2426
def __init__(
2527
self,
28+
conn: sqlite3.Connection,
29+
section: "CacheSection",
30+
entry_name: str,
2631
meta_blob: Optional[bytes],
27-
data_blob: bytes,
2832
meta_type: Union[Type[_M], Tuple[Type[_M], ...]],
2933
data_type: Union[Type[_D], Tuple[Type[_D], ...]],
3034
) -> None:
35+
self._conn = conn
36+
self._section = section
37+
self._entry_name = entry_name
3138
self._meta_blob = meta_blob
32-
self._data_blob = data_blob
3339
self._meta_type = meta_type
3440
self._data_type = data_type
3541
self._meta_cache: Optional[_M] = None
@@ -51,7 +57,13 @@ def meta(self) -> Optional[_M]:
5157
@property
5258
def data(self) -> _D:
5359
if not self._data_loaded:
54-
result = pickle.loads(self._data_blob)
60+
row = self._conn.execute(
61+
f"SELECT data FROM {self._section.value} WHERE entry_name = ?",
62+
(self._entry_name,),
63+
).fetchone()
64+
if row is None:
65+
raise RuntimeError(f"Cache entry '{self._entry_name}' disappeared from DB")
66+
result = pickle.loads(row[0])
5567
if not isinstance(result, self._data_type):
5668
raise TypeError(f"Expected {self._data_type} but got {type(result)}")
5769
self._data_cache = cast(_D, result)
@@ -116,14 +128,14 @@ def read_entry(
116128
data_type: Union[Type[_D], Tuple[Type[_D], ...]],
117129
) -> Optional[CacheEntry[_M, _D]]:
118130
row = self._conn.execute(
119-
f"SELECT meta, data FROM {section.value} WHERE entry_name = ?",
131+
f"SELECT meta FROM {section.value} WHERE entry_name = ?",
120132
(entry_name,),
121133
).fetchone()
122134

123135
if row is None:
124136
return None
125137

126-
return CacheEntry(row[0], row[1], meta_type, data_type)
138+
return CacheEntry(self._conn, section, entry_name, row[0], meta_type, data_type)
127139

128140
def save_entry(
129141
self,

tests/robotcode/robot/diagnostics/test_data_cache.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from dataclasses import dataclass
44
from pathlib import Path
5+
from typing import Optional
56

67
import pytest
78

@@ -21,12 +22,31 @@ class _SampleData:
2122

2223

2324
class TestCacheEntry:
25+
def _make_entry(
26+
self,
27+
meta_blob: "Optional[bytes]",
28+
data_blob: bytes,
29+
meta_type: type,
30+
data_type: type,
31+
) -> CacheEntry: # type: ignore[type-arg]
32+
"""Create a CacheEntry backed by a temporary in-memory DB."""
33+
import sqlite3
34+
35+
conn = sqlite3.connect(":memory:")
36+
table = CacheSection.LIBRARY.value
37+
conn.execute(f"CREATE TABLE {table} (entry_name TEXT PRIMARY KEY, meta BLOB, data BLOB NOT NULL)")
38+
conn.execute(
39+
f"INSERT INTO {table} (entry_name, meta, data) VALUES (?, ?, ?)",
40+
("test_entry", meta_blob, data_blob),
41+
)
42+
return CacheEntry(conn, CacheSection.LIBRARY, "test_entry", meta_blob, meta_type, data_type)
43+
2444
def test_lazy_meta_deserialization(self) -> None:
2545
import pickle
2646

2747
meta = _SampleMeta(name="test", version=1)
2848
data = _SampleData(name="hello", value=42)
29-
entry = CacheEntry(
49+
entry = self._make_entry(
3050
pickle.dumps(meta),
3151
pickle.dumps(data),
3252
_SampleMeta,
@@ -40,7 +60,7 @@ def test_lazy_data_deserialization(self) -> None:
4060
import pickle
4161

4262
data = _SampleData(name="hello", value=42)
43-
entry = CacheEntry(
63+
entry = self._make_entry(
4464
None,
4565
pickle.dumps(data),
4666
_SampleMeta,
@@ -52,7 +72,7 @@ def test_lazy_data_deserialization(self) -> None:
5272
def test_meta_type_mismatch_raises(self) -> None:
5373
import pickle
5474

55-
entry = CacheEntry(
75+
entry = self._make_entry(
5676
pickle.dumps("not a meta"),
5777
pickle.dumps("data"),
5878
_SampleMeta,
@@ -64,7 +84,7 @@ def test_meta_type_mismatch_raises(self) -> None:
6484
def test_data_type_mismatch_raises(self) -> None:
6585
import pickle
6686

67-
entry = CacheEntry(
87+
entry = self._make_entry(
6888
None,
6989
pickle.dumps("not data"),
7090
_SampleMeta,

0 commit comments

Comments
 (0)