7
7
from openhands .events .serialization .event import event_from_dict , event_to_dict
8
8
from openhands .storage .files import FileStore
9
9
from openhands .storage .locations import (
10
+ get_conversation_dir ,
10
11
get_conversation_event_filename ,
11
12
get_conversation_events_dir ,
12
13
)
13
14
from openhands .utils .shutdown_listener import should_continue
14
15
15
16
17
+ @dataclass (frozen = True )
18
+ class _CachePage :
19
+ events : list [dict ] | None
20
+ start : int
21
+ end : int
22
+
23
+ def covers (self , global_index : int ) -> bool :
24
+ if global_index < self .start :
25
+ return False
26
+ if global_index >= self .end :
27
+ return False
28
+ return True
29
+
30
+ def get_event (self , global_index : int ) -> Event | None :
31
+ # If there was not actually a cached page, return None
32
+ if not self .events :
33
+ return None
34
+ local_index = global_index - self .start
35
+ return event_from_dict (self .events [local_index ])
36
+
37
+
38
+ _DUMMY_PAGE = _CachePage (None , 1 , - 1 )
39
+
40
+
16
41
@dataclass
17
42
class EventStore :
18
43
"""
@@ -23,6 +48,7 @@ class EventStore:
23
48
file_store : FileStore
24
49
user_id : str | None
25
50
cur_id : int = - 1 # We fix this in post init if it is not specified
51
+ cache_size : int = 25
26
52
27
53
def __post_init__ (self ) -> None :
28
54
if self .cur_id >= 0 :
@@ -83,30 +109,33 @@ def should_filter(event: Event) -> bool:
83
109
return True
84
110
return False
85
111
112
+ if end_id is None :
113
+ end_id = self .cur_id
114
+ else :
115
+ end_id += 1 # From inclusive to exclusive
116
+
86
117
if reverse :
87
- if end_id is None :
88
- end_id = self .cur_id - 1
89
- event_id = end_id
90
- while event_id >= start_id :
91
- try :
92
- event = self .get_event (event_id )
93
- if not should_filter (event ):
94
- yield event
95
- except FileNotFoundError :
96
- logger .debug (f'No event found for ID { event_id } ' )
97
- event_id -= 1
118
+ step = - 1
119
+ start_id , end_id = end_id , start_id
120
+ start_id -= 1
121
+ end_id -= 1
98
122
else :
99
- event_id = start_id
100
- while should_continue ():
101
- if end_id is not None and event_id > end_id :
102
- break
123
+ step = 1
124
+
125
+ cache_page = _DUMMY_PAGE
126
+ for index in range (start_id , end_id , step ):
127
+ if not should_continue ():
128
+ return
129
+ if not cache_page .covers (index ):
130
+ cache_page = self ._load_cache_page_for_index (index )
131
+ event = cache_page .get_event (index )
132
+ if event is None :
103
133
try :
104
- event = self .get_event (event_id )
105
- if not should_filter (event ):
106
- yield event
134
+ event = self .get_event (index )
107
135
except FileNotFoundError :
108
- break
109
- event_id += 1
136
+ event = None
137
+ if event and not should_filter (event ):
138
+ yield event
110
139
111
140
def get_event (self , id : int ) -> Event :
112
141
filename = self ._get_filename_for_id (id , self .user_id )
@@ -230,6 +259,25 @@ def get_matching_events(
230
259
def _get_filename_for_id (self , id : int , user_id : str | None ) -> str :
231
260
return get_conversation_event_filename (self .sid , id , user_id )
232
261
262
+ def _get_filename_for_cache (self , start : int , end : int ) -> str :
263
+ return f'{ get_conversation_dir (self .sid , self .user_id )} event_cache/{ start } -{ end } .json'
264
+
265
+ def _load_cache_page (self , start : int , end : int ) -> _CachePage :
266
+ """Read a page from the cache. Reading individual events is slow when there are a lot of them, so we use pages."""
267
+ cache_filename = self ._get_filename_for_cache (start , end )
268
+ try :
269
+ content = self .file_store .read (cache_filename )
270
+ events = json .loads (content )
271
+ except FileNotFoundError :
272
+ events = None
273
+ page = _CachePage (events , start , end )
274
+ return page
275
+
276
+ def _load_cache_page_for_index (self , index : int ) -> _CachePage :
277
+ offset = index % self .cache_size
278
+ index -= offset
279
+ return self ._load_cache_page (index , index + self .cache_size )
280
+
233
281
@staticmethod
234
282
def _get_id_from_filename (filename : str ) -> int :
235
283
try :
0 commit comments