From 4b37511ec8ddd13858dc7645a40b8c7395845109 Mon Sep 17 00:00:00 2001 From: spomichter Date: Tue, 10 Mar 2026 11:42:49 +0000 Subject: [PATCH 1/2] fix: rate-limit rerun bridge to prevent viewer OOM Add per-entity-path rate limiting (default 10 Hz) to _on_message. Without this, 30fps camera streams flood the viewer with data faster than its memory eviction can keep up, causing unbounded memory growth in the renderer's texture cache (not governed by memory_limit). Configurable via min_interval_sec on the bridge Config. Set to 0 to disable rate limiting for low-bandwidth deployments. --- dimos/visualization/rerun/bridge.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dimos/visualization/rerun/bridge.py b/dimos/visualization/rerun/bridge.py index 0e11981bc6..d40ef9c555 100644 --- a/dimos/visualization/rerun/bridge.py +++ b/dimos/visualization/rerun/bridge.py @@ -18,6 +18,7 @@ from dataclasses import dataclass, field from functools import lru_cache +import time from typing import ( TYPE_CHECKING, Any, @@ -171,6 +172,7 @@ class Config(ModuleConfig): # Static items logged once after start. Maps entity_path -> callable(rr) returning Archetype static: dict[str, Callable[[Any], Archetype]] = field(default_factory=dict) + min_interval_sec: float = 0.1 # Rate-limit per entity path (default: 10 Hz max) entity_prefix: str = "world" topic_to_entity: Callable[[Any], str] | None = None viewer_mode: ViewerMode = field(default_factory=_resolve_viewer_mode) @@ -254,6 +256,18 @@ def _on_message(self, msg: Any, topic: Any) -> None: # convert a potentially complex topic object into an str rerun entity path entity_path: str = self._get_entity_path(topic) + # Rate-limit per entity path to prevent viewer memory exhaustion. + # High-bandwidth streams (e.g. 30fps camera) would otherwise flood + # the viewer with data faster than it can evict, causing OOM. + if self.config.min_interval_sec > 0: + now = time.monotonic() + if not hasattr(self, "_last_log"): + self._last_log: dict[str, float] = {} + last = self._last_log.get(entity_path, 0.0) + if now - last < self.config.min_interval_sec: + return + self._last_log[entity_path] = now + # apply visual overrides (including final_convert which handles .to_rerun()) rerun_data: RerunData | None = self._visual_override_for_entity_path(entity_path)(msg) From 72c3fc630108251d11941155385f7b63076b5c91 Mon Sep 17 00:00:00 2001 From: spomichter Date: Tue, 10 Mar 2026 13:31:50 +0000 Subject: [PATCH 2/2] fix: init _last_log in start() instead of lazy hasattr check Move rate limiter dict initialization from per-message hasattr check to start(), before subscriptions begin. Removes per-call overhead and a potential race where two threads could overwrite each other's dict. --- dimos/visualization/rerun/bridge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dimos/visualization/rerun/bridge.py b/dimos/visualization/rerun/bridge.py index d40ef9c555..078a2a2ef4 100644 --- a/dimos/visualization/rerun/bridge.py +++ b/dimos/visualization/rerun/bridge.py @@ -261,8 +261,6 @@ def _on_message(self, msg: Any, topic: Any) -> None: # the viewer with data faster than it can evict, causing OOM. if self.config.min_interval_sec > 0: now = time.monotonic() - if not hasattr(self, "_last_log"): - self._last_log: dict[str, float] = {} last = self._last_log.get(entity_path, 0.0) if now - last < self.config.min_interval_sec: return @@ -288,6 +286,7 @@ def start(self) -> None: super().start() + self._last_log: dict[str, float] = {} logger.info("Rerun bridge starting", viewer_mode=self.config.viewer_mode) # Initialize and spawn Rerun viewer