diff --git a/hippolyzer/apps/proxy_gui.py b/hippolyzer/apps/proxy_gui.py index 71f3e05..746fa2a 100644 --- a/hippolyzer/apps/proxy_gui.py +++ b/hippolyzer/apps/proxy_gui.py @@ -196,8 +196,10 @@ class ProxyGUI(QtWidgets.QMainWindow): self.actionProxyRemotelyAccessible.setChecked(self.settings.REMOTELY_ACCESSIBLE) self.actionUseViewerObjectCache.setChecked(self.settings.USE_VIEWER_OBJECT_CACHE) + self.actionRequestMissingObjects.setChecked(self.settings.AUTOMATICALLY_REQUEST_MISSING_OBJECTS) self.actionProxyRemotelyAccessible.triggered.connect(self._setProxyRemotelyAccessible) self.actionUseViewerObjectCache.triggered.connect(self._setUseViewerObjectCache) + self.actionRequestMissingObjects.triggered.connect(self._setRequestMissingObjects) self._filterMenu = QtWidgets.QMenu() self._populateFilterMenu() @@ -382,6 +384,9 @@ class ProxyGUI(QtWidgets.QMainWindow): def _setUseViewerObjectCache(self, checked: bool): self.sessionManager.settings.USE_VIEWER_OBJECT_CACHE = checked + def _setRequestMissingObjects(self, checked: bool): + self.sessionManager.settings.AUTOMATICALLY_REQUEST_MISSING_OBJECTS = checked + def _manageAddons(self): dialog = AddonDialog(self) dialog.exec_() diff --git a/hippolyzer/apps/proxy_mainwindow.ui b/hippolyzer/apps/proxy_mainwindow.ui index 53b548a..ae0708d 100644 --- a/hippolyzer/apps/proxy_mainwindow.ui +++ b/hippolyzer/apps/proxy_mainwindow.ui @@ -263,6 +263,7 @@ + @@ -311,6 +312,17 @@ Can help make the proxy aware of certain objects, but can cause slowdowns + + + true + + + Automatically Request Missing Objects + + + Force the proxy to request objects that it doesn't know about due to cache misses + + diff --git a/hippolyzer/lib/client/object_manager.py b/hippolyzer/lib/client/object_manager.py index 56bd3a5..7cada20 100644 --- a/hippolyzer/lib/client/object_manager.py +++ b/hippolyzer/lib/client/object_manager.py @@ -111,12 +111,12 @@ class ClientObjectManager: while ids_to_req: blocks = [ Block("AgentData", AgentID=session.agent_id, SessionID=session.id), - *[Block("ObjectData", ObjectLocalID=x) for x in ids_to_req[:100]], + *[Block("ObjectData", ObjectLocalID=x) for x in ids_to_req[:255]], ] # Selecting causes ObjectProperties to be sent self._region.circuit.send_message(Message("ObjectSelect", blocks)) self._region.circuit.send_message(Message("ObjectDeselect", blocks)) - ids_to_req = ids_to_req[100:] + ids_to_req = ids_to_req[255:] futures = [] for local_id in local_ids: @@ -151,9 +151,9 @@ class ClientObjectManager: self._region.circuit.send_message(Message( "RequestMultipleObjects", Block("AgentData", AgentID=session.agent_id, SessionID=session.id), - *[Block("ObjectData", CacheMissType=0, ID=x) for x in ids_to_req[:100]], + *[Block("ObjectData", CacheMissType=0, ID=x) for x in ids_to_req[:255]], )) - ids_to_req = ids_to_req[100:] + ids_to_req = ids_to_req[255:] futures = [] for local_id in local_ids: @@ -454,15 +454,17 @@ class ClientWorldObjectManager: missing_locals.add(block["ID"]) if region_state: region_state.missing_locals.update(missing_locals) - self._handle_object_update_cached_misses(handle, missing_locals) + if missing_locals: + self._handle_object_update_cached_misses(handle, missing_locals) msg.meta["ObjectUpdateIDs"] = tuple(seen_locals) - def _handle_object_update_cached_misses(self, region_handle: int, local_ids: Set[int]): + def _handle_object_update_cached_misses(self, region_handle: int, missing_locals: Set[int]): + """Handle an ObjectUpdateCached that referenced some un-cached local IDs""" region_mgr = self._get_region_manager(region_handle) - region_mgr.request_objects(local_ids) + region_mgr.request_objects(missing_locals) # noinspection PyUnusedLocal - def _lookup_cache_entry(self, handle: int, local_id: int, crc: int) -> Optional[bytes]: + def _lookup_cache_entry(self, region_handle: int, local_id: int, crc: int) -> Optional[bytes]: return None def _handle_object_update_compressed(self, msg: Message): diff --git a/hippolyzer/lib/proxy/object_manager.py b/hippolyzer/lib/proxy/object_manager.py index 01d0f4e..8cbc60f 100644 --- a/hippolyzer/lib/proxy/object_manager.py +++ b/hippolyzer/lib/proxy/object_manager.py @@ -1,9 +1,11 @@ from __future__ import annotations +import asyncio import logging from typing import * from hippolyzer.lib.base import llsd +from hippolyzer.lib.base.message.message import Message from hippolyzer.lib.client.namecache import NameCache from hippolyzer.lib.client.object_manager import ( ClientObjectManager, @@ -38,6 +40,12 @@ class ProxyObjectManager(ClientObjectManager): self.may_use_vo_cache = may_use_vo_cache self.cache_loaded = False self.object_cache = RegionViewerObjectCacheChain([]) + self._cache_miss_timer: Optional[asyncio.TimerHandle] = None + self.queued_cache_misses: Set[int] = set() + region.message_handler.subscribe( + "RequestMultipleObjects", + self._handle_request_multiple_objects, + ) def load_cache(self): if not self.may_use_vo_cache or self.cache_loaded: @@ -49,17 +57,40 @@ class ProxyObjectManager(ClientObjectManager): self.cache_loaded = True self.object_cache = RegionViewerObjectCacheChain.for_region(handle, self._region.cache_id) + def request_missed_cached_objects_soon(self): + if self._cache_miss_timer: + self._cache_miss_timer.cancel() + # Basically debounce. Will only trigger 0.2 seconds after the last time it's invoked to + # deal with the initial flood of ObjectUpdateCached and the natural lag time between that + # and the viewers' RequestMultipleObjects messages + self._cache_miss_timer = asyncio.get_event_loop().call_later( + 0.2, self._request_missed_cached_objects) + + def _request_missed_cached_objects(self): + self._cache_miss_timer = None + self.request_objects(self.queued_cache_misses) + self.queued_cache_misses.clear() + def clear(self): super().clear() self.object_cache = RegionViewerObjectCacheChain([]) self.cache_loaded = False + self.queued_cache_misses.clear() + if self._cache_miss_timer: + self._cache_miss_timer.cancel() + self._cache_miss_timer = None def _is_localid_selected(self, localid: int): return localid in self._region.session().selected.object_locals + def _handle_request_multiple_objects(self, msg: Message): + # Remove any queued cache misses that the viewer just requested for itself + self.queued_cache_misses -= {b["ID"] for b in msg["ObjectData"]} + class ProxyWorldObjectManager(ClientWorldObjectManager): _session: Session + _settings: ProxySettings def __init__(self, session: Session, settings: ProxySettings, name_cache: Optional[NameCache]): super().__init__(session, settings, name_cache) @@ -68,10 +99,17 @@ class ProxyWorldObjectManager(ClientWorldObjectManager): self._handle_get_object_cost ) - def _handle_object_update_cached_misses(self, region_handle: int, local_ids: Set[int]): - # Don't do anything automatically. People have to manually ask for - # missed objects to be fetched. - pass + def _handle_object_update_cached_misses(self, region_handle: int, missing_locals: Set[int]): + if self._settings.AUTOMATICALLY_REQUEST_MISSING_OBJECTS: + # Schedule these local IDs to be requested soon if the viewer doesn't request + # them itself. Ideally we could just mutate the CRC of the ObjectUpdateCached + # to force a CRC cache miss in the viewer, but that appears to cause the viewer + # to drop the resulting ObjectUpdateCompressed when the CRC doesn't match? + # It was causing all objects to go missing even though the ObjectUpdateCompressed + # was received. + region_mgr: Optional[ProxyObjectManager] = self._get_region_manager(region_handle) + region_mgr.queued_cache_misses |= missing_locals + region_mgr.request_missed_cached_objects_soon() def _run_object_update_hooks(self, obj: Object, updated_props: Set[str], update_type: UpdateType): super()._run_object_update_hooks(obj, updated_props, update_type) @@ -83,8 +121,8 @@ class ProxyWorldObjectManager(ClientWorldObjectManager): region = self._session.region_by_handle(obj.RegionHandle) AddonManager.handle_object_killed(self._session, region, obj) - def _lookup_cache_entry(self, handle: int, local_id: int, crc: int) -> Optional[bytes]: - region_mgr: Optional[ProxyObjectManager] = self._get_region_manager(handle) + def _lookup_cache_entry(self, region_handle: int, local_id: int, crc: int) -> Optional[bytes]: + region_mgr: Optional[ProxyObjectManager] = self._get_region_manager(region_handle) return region_mgr.object_cache.lookup_object_data(local_id, crc) def _handle_get_object_cost(self, flow: HippoHTTPFlow): diff --git a/hippolyzer/lib/proxy/settings.py b/hippolyzer/lib/proxy/settings.py index 4ae166f..46e1afa 100644 --- a/hippolyzer/lib/proxy/settings.py +++ b/hippolyzer/lib/proxy/settings.py @@ -28,5 +28,6 @@ class ProxySettings(Settings): PROXY_BIND_ADDR: str = EnvSettingDescriptor("127.0.0.1", "HIPPO_BIND_HOST", str) REMOTELY_ACCESSIBLE: bool = SettingDescriptor(False) USE_VIEWER_OBJECT_CACHE: bool = SettingDescriptor(False) + AUTOMATICALLY_REQUEST_MISSING_OBJECTS: bool = SettingDescriptor(False) ADDON_SCRIPTS: List[str] = SettingDescriptor(list) FILTERS: Dict[str, str] = SettingDescriptor(dict)