Skip to content

Commit cf677e2

Browse files
authored
Merge pull request #917 from dimensionalOS/feat/rerun-latency-panels
Feat/rerun latency panels Former-commit-id: a3a03b3 [formerly 72935f8] Former-commit-id: 41a54dc
1 parent 12189e6 commit cf677e2

40 files changed

+2883
-82
lines changed

dimos/core/blueprints.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
from types import MappingProxyType
2424
from typing import Any, Literal, get_args, get_origin, get_type_hints
2525

26+
import rerun as rr
27+
import rerun.blueprint as rrb
28+
2629
from dimos.core.global_config import GlobalConfig
2730
from dimos.core.module import Module
2831
from dimos.core.module_coordinator import ModuleCoordinator
@@ -280,6 +283,50 @@ def _connect_rpc_methods(self, module_coordinator: ModuleCoordinator) -> None:
280283
requested_method_name, rpc_methods_dot[requested_method_name]
281284
)
282285

286+
def _init_rerun_blueprint(self, module_coordinator: ModuleCoordinator) -> None:
287+
"""Compose and send Rerun blueprint from module contributions.
288+
289+
Collects rerun_views() from all modules and composes them into a unified layout.
290+
"""
291+
# Collect view contributions from all modules
292+
side_panels = []
293+
for blueprint in self.blueprints:
294+
if hasattr(blueprint.module, "rerun_views"):
295+
views = blueprint.module.rerun_views()
296+
if views:
297+
side_panels.extend(views)
298+
299+
# Always include latency panel if we have any panels
300+
if side_panels:
301+
side_panels.append(
302+
rrb.TimeSeriesView(
303+
name="Latency (ms)",
304+
origin="/metrics",
305+
contents=[
306+
"+ /metrics/voxel_map/latency_ms",
307+
"+ /metrics/costmap/latency_ms",
308+
],
309+
)
310+
)
311+
312+
# Compose final layout
313+
if side_panels:
314+
composed_blueprint = rrb.Blueprint(
315+
rrb.Horizontal(
316+
rrb.Spatial3DView(
317+
name="3D View",
318+
origin="world",
319+
background=[0, 0, 0],
320+
),
321+
rrb.Vertical(*side_panels, row_shares=[2] + [1] * (len(side_panels) - 1)),
322+
column_shares=[3, 1],
323+
),
324+
rrb.TimePanel(state="collapsed"),
325+
rrb.SelectionPanel(state="collapsed"),
326+
rrb.BlueprintPanel(state="collapsed"),
327+
)
328+
rr.send_blueprint(composed_blueprint)
329+
283330
def build(
284331
self,
285332
global_config: GlobalConfig | None = None,
@@ -294,6 +341,17 @@ def build(
294341
self._check_requirements()
295342
self._verify_no_name_conflicts()
296343

344+
# Initialize Rerun server before deploying modules (if backend is Rerun)
345+
if global_config.rerun_enabled and global_config.viewer_backend.startswith("rerun"):
346+
try:
347+
from dimos.dashboard.rerun_init import init_rerun_server
348+
349+
server_addr = init_rerun_server(viewer_mode=global_config.viewer_backend)
350+
global_config = global_config.model_copy(update={"rerun_server_addr": server_addr})
351+
logger.info("Rerun server initialized", addr=server_addr)
352+
except Exception as e:
353+
logger.warning(f"Failed to initialize Rerun server: {e}")
354+
297355
module_coordinator = ModuleCoordinator(global_config=global_config)
298356
module_coordinator.start()
299357

@@ -303,6 +361,10 @@ def build(
303361

304362
module_coordinator.start_all_modules()
305363

364+
# Compose and send Rerun blueprint from module contributions
365+
if global_config.viewer_backend.startswith("rerun"):
366+
self._init_rerun_blueprint(module_coordinator)
367+
306368
return module_coordinator
307369

308370

dimos/core/global_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414

1515
from functools import cached_property
1616
import re
17+
from typing import Literal, TypeAlias
1718

1819
from pydantic_settings import BaseSettings, SettingsConfigDict
1920

2021
from dimos.mapping.occupancy.path_map import NavigationStrategy
2122
from dimos.navigation.global_planner.types import AStarAlgorithm
2223

24+
ViewerBackend: TypeAlias = Literal["rerun-web", "rerun-native", "foxglove"]
25+
2326

2427
def _get_all_numbers(s: str) -> list[float]:
2528
return [float(x) for x in re.findall(r"-?\d+\.?\d*", s)]
@@ -29,6 +32,9 @@ class GlobalConfig(BaseSettings):
2932
robot_ip: str | None = None
3033
simulation: bool = False
3134
replay: bool = False
35+
rerun_enabled: bool = True
36+
rerun_server_addr: str | None = None
37+
viewer_backend: ViewerBackend = "rerun-native"
3238
n_dask_workers: int = 2
3339
memory_limit: str = "auto"
3440
mujoco_camera_position: str | None = None

dimos/core/stream.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import reactivex as rx
2727
from reactivex import operators as ops
2828
from reactivex.disposable import Disposable
29+
import rerun as rr
2930

3031
import dimos.core.colors as colors
3132
from dimos.core.resource import Resource
@@ -137,20 +138,21 @@ def __str__(self) -> str:
137138
)
138139

139140

140-
class Out(Stream[T]):
141+
class Out(Stream[T], ObservableMixin[T]):
141142
_transport: Transport # type: ignore[type-arg]
142143

143144
def __init__(self, *argv, **kwargs) -> None: # type: ignore[no-untyped-def]
144145
super().__init__(*argv, **kwargs)
146+
self._rerun_config: dict | None = None # type: ignore[type-arg]
147+
self._rerun_last_log: float = 0.0
145148

146149
@property
147150
def transport(self) -> Transport[T]:
148151
return self._transport
149152

150153
@transport.setter
151154
def transport(self, value: Transport[T]) -> None:
152-
# just for type checking
153-
...
155+
self._transport = value
154156

155157
@property
156158
def state(self) -> State:
@@ -173,8 +175,76 @@ def publish(self, msg) -> None: # type: ignore[no-untyped-def]
173175
if not hasattr(self, "_transport") or self._transport is None:
174176
logger.warning(f"Trying to publish on Out {self} without a transport")
175177
return
178+
179+
# Log to Rerun directly if configured
180+
if self._rerun_config is not None:
181+
self._log_to_rerun(msg)
182+
176183
self._transport.broadcast(self, msg)
177184

185+
def subscribe(self, cb) -> Callable[[], None]: # type: ignore[no-untyped-def]
186+
"""Subscribe to this output stream.
187+
188+
Args:
189+
cb: Callback function to receive messages
190+
191+
Returns:
192+
Unsubscribe function
193+
"""
194+
return self.transport.subscribe(cb, self) # type: ignore[arg-type, func-returns-value, no-any-return]
195+
196+
def autolog_to_rerun(
197+
self,
198+
entity_path: str,
199+
rate_limit: float | None = None,
200+
**rerun_kwargs: Any,
201+
) -> None:
202+
"""Configure this output to auto-log to Rerun (fire-and-forget).
203+
204+
Call once in start() - messages auto-logged when published.
205+
206+
Args:
207+
entity_path: Rerun entity path (e.g., "world/map")
208+
rate_limit: Max Hz to log (None = unlimited)
209+
**rerun_kwargs: Passed to msg.to_rerun() for rendering config
210+
(e.g., radii=0.02, colormap="turbo", colors=[255,0,0])
211+
212+
Example:
213+
def start(self):
214+
super().start()
215+
# Just declare it - fire and forget!
216+
self.global_map.autolog_to_rerun("world/map", rate_limit=5.0, radii=0.02)
217+
"""
218+
self._rerun_config = {
219+
"entity_path": entity_path,
220+
"rate_limit": rate_limit,
221+
"rerun_kwargs": rerun_kwargs,
222+
}
223+
self._rerun_last_log = 0.0
224+
225+
def _log_to_rerun(self, msg: T) -> None:
226+
"""Log message to Rerun with rate limiting."""
227+
if not hasattr(msg, "to_rerun"):
228+
return
229+
230+
if self._rerun_config is None:
231+
return
232+
233+
import time
234+
235+
config = self._rerun_config
236+
237+
# Rate limiting
238+
if config["rate_limit"] is not None:
239+
now = time.monotonic()
240+
min_interval = 1.0 / config["rate_limit"]
241+
if now - self._rerun_last_log < min_interval:
242+
return # Skip - too soon
243+
self._rerun_last_log = now
244+
245+
rerun_data = msg.to_rerun(**config["rerun_kwargs"])
246+
rr.log(config["entity_path"], rerun_data)
247+
178248

179249
class RemoteStream(Stream[T]):
180250
@property

dimos/dashboard/__init__.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2025 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Dashboard module for visualization and monitoring.
16+
17+
Rerun Initialization:
18+
Main process (e.g., blueprints.build) starts Rerun server automatically.
19+
Worker modules connect to the server via connect_rerun().
20+
21+
Usage in modules:
22+
import rerun as rr
23+
from dimos.dashboard.rerun_init import connect_rerun
24+
25+
class MyModule(Module):
26+
def start(self):
27+
super().start()
28+
connect_rerun() # Connect to Rerun server
29+
rr.log("my/entity", my_data.to_rerun())
30+
"""
31+
32+
from dimos.dashboard.rerun_init import connect_rerun, init_rerun_server, shutdown_rerun
33+
34+
__all__ = ["connect_rerun", "init_rerun_server", "shutdown_rerun"]

dimos/dashboard/dimos.rbl

153 KB
Binary file not shown.

0 commit comments

Comments
 (0)