ros2probe is a middleware-level observability layer for ROS 2. It taps wire traffic from the kernel network path, never creates a middleware participant, and reconstructs the full ROS graph, topic metrics, and message streams entirely in userspace. DDS is supported today. Zenoh is planned.
Existing graph, CLI, and recording tools all join the middleware graph. They perturb the very properties engineers are trying to measure.
Runs as a middleware participant to cache the graph, continuously participating in discovery. Contributes to O(N²) discovery traffic and exhibits graph-state inconsistency when nodes crash or networks partition.
Creates a new subscriber just to observe. Triggers QoS negotiation, injects receive-side load, and appears in ros2 topic list. The act of measurement deforms the system.
Registers as a full middleware participant with subscribers for every recorded topic. Observation and storage cannot be separated. Under constrained links the recorder itself starves the control plane.
ros2probe sits outside the middleware runtime. It reads wire frames at the kernel network path, filters them on a session-driven endpoint whitelist, and falls back gracefully when IP fragmentation or shared-memory transports get in the way. Today we parse the RTPS wire format for DDS; the same architecture will absorb the Zenoh wire format as a parser plug-in.
Passive Observer architecture. No middleware participant is created at steady state. Logical topology integrity is preserved. Discovery, QoS negotiation, and data-plane load are untouched.
Session-level topic interest compiles into a kernel filter. In fragmentation-avoided deployments: perfect per-packet selectivity. Otherwise: a fragment-aware fallback that preserves correctness without assuming any specific middleware configuration.
Data-centric, event-driven GID synchronisation: each SEDP announcement immediately updates the kernel whitelist. RTPS protocol ordering guarantees the GID is registered before any DATA can arrive, eliminating races without a time-based window.
The main observer stays zero-participant. When the user explicitly observes a shared-memory topic, ros2probe spawns a short-lived, namespace-isolated subprocess that joins the DDS domain only inside its mount namespace — the runtime itself never becomes a middleware participant.
Switch between tabs for the layered architecture, the feature surface, live demos, and the paper.
Every other ROS 2 observability tool attaches somewhere inside the application stack: rclcpp, rcl, rmw, or the middleware itself. ros2probe is the only layer that sits below the middleware, in the kernel network path.
ros2_tracing adds tracepoints in rclcpp / rcl. CARET wraps rmw with LD_PRELOAD. rosbag2, ros2 topic echo, ddsrecorder, and FastDDS Statistics all create endpoints in the middleware itself. Each becomes part of the stack being observed.
A kernel-space eBPF socket filter sees wire frames as they leave or enter the NIC. Userspace reassembles, decodes, and reconstructs the graph. The middleware never sees ros2probe.
Kernel timestamps arrive with each frame. Loopback is included by default so intra-host UDP traffic is captured too.
Vendor-neutral across DDS implementations. OMG RTPS is the only ABI. The Zenoh wire format will plug in at the same layer.
ros_discovery_info binds endpoint GUIDs to ROS node namespaces. TTL sweep keeps state fresh when nodes disappear.
A single Unix socket command bus multiplexes all consumers. Sessions open and close, the kernel filter follows automatically.
ros2probe never joins the middleware graph. Your ros2 topic list stays clean.
Participants, endpoints, node names, pub/sub relationships, all derived from wire discovery and ros_discovery_info.
SHM-only topics are identified and made observable on demand via namespace-isolated shadow subscribers.
Publish rate (hz), bandwidth (bw), end-to-end delay, and decoded echo, all with sliding-window statistics.
Bag files with optional zstd / lz4 compression, pause / resume, and per-topic selection.
Same data, two interfaces. rp for shells and scripts, rp gui for visual exploration.
tf, parameter_events, rosout, debug, and SHM-only topics are automatically classified and separated in every UI.
Each SEDP event immediately updates the kernel GID whitelist. RTPS protocol ordering ensures the GID is registered before DATA arrives — no time-based window needed.
Wire-level observation only. No vendor-specific symbols, no middleware version coupling, no BTF/CO-RE fragility.
# auto-escalates to root if needed (CAP_NET_ADMIN + CAP_NET_RAW) rp run
rp topic list rp topic info /chatter rp topic hz /chatter rp topic bw /scan rp topic delay /imu rp topic echo /chatter --field data
rp bag record /chatter /scan -o session.mcap --compression-format zstd
rp gui
Not all of these tools are built for the same thing. ros2_tracing and CARET are performance-analysis tools for callback chains and executors. rosbag2 and ddsrecorder are loggers. ros2 daemon is a CLI cache. ros2probe aims at live middleware-level observation. The table below makes this explicit in the first row.
| ros2 daemon | ros2 topic | rosbag2 | ros2_tracing | CARET | ros2probe | |
|---|---|---|---|---|---|---|
| Purpose | CLI graph cache | Live topic CLI | Message logging | Callback / executor tracing | E2E callback-chain latency | Live middleware-level observation |
| Graph observation | △ | ✗ | ✗ | ✗ | △ | ● |
| Message recording | ✗ | ✗ | ● | ✗ | ✗ | ● |
| Callback / executor trace | ✗ | ✗ | ✗ | ● | ● | ✗ |
| Non-intrusive | ✗ | ✗ | ✗ | △ | △ | ● |
| Process restart not required | ● | ● | ● | ● | ✗ | ● |
| Middleware-neutral | ✗ | ✗ | △ | △ | ✗ | ● |
| Steady-state overhead | high | none (off) | none (off) | low | high | minimal |
| Observer Effect on graph | yes | yes | yes | limited | limited | none |
● full support · △ partial / conditional · ✗ not supported
ros2_tracing and CARET solve a different problem (internal performance analysis) and are complementary to ros2probe, not competitors.
Abstract
Observability tools for ROS 2 are themselves observed: existing daemons, ros2 topic CLIs, and rosbag2 recorders all join the middleware graph and perturb discovery, QoS negotiation, and data-plane load. These are precisely the properties engineers try to measure.
We present ros2probe, a middleware-level observability architecture that taps wire frames at the kernel network path with an eBPF socket filter, never creating a middleware participant at steady state. The design contributes (i) a passive observer that preserves logical topology integrity, (ii) adaptive kernel-side semantic selectivity, (iii) discovery race-free selectivity through data-centric, event-driven GID synchronisation, and (iv) an adaptive visibility strategy for shared-memory blind spots.
On a real testbed of laptops, a Jetson Orin Nano, and a Raspberry Pi running teleoperation, IMU, and sensor-grade LiDAR and camera workloads derived from actual product specifications (Velodyne / Ouster / Hesai LiDAR tiers, 720p / 1080p RGB and depth streams), ros2probe eliminates observer-induced network overhead, contributes zero SPDP / SEDP traffic beyond ros2 daemon, lowers CPU and RSS cost relative to both ros2 daemon and the active rosbag2 / ros2 topic hz tools, and sustains a drop-rate saturation frontier at least on par with rosbag2 under a synthetic rate × size stress sweep. The architecture is not tied to any specific middleware: RTPS is supported today, Zenoh is planned.
Passive Observer architecture. No middleware participant at steady state. The monitoring tool does not become part of what it measures.
Session-level topic interest compiles to a kernel filter. Fragment-aware fallback preserves correctness regardless of middleware configuration.
Data-centric, event-driven GID synchronisation: each SEDP announcement immediately updates the kernel whitelist. RTPS protocol ordering structurally prevents discovery-vs-data races.
The main observer stays zero-participant. SHM-only topics become visible on demand: ros2probe launches a short-lived subprocess inside an isolated mount namespace, and only that subprocess joins the DDS domain. The runtime itself never registers as a middleware participant.
Experiments run on a real testbed (laptops, Jetson Orin Nano, Raspberry Pi 4/5) with four workloads derived from actual sensor product specifications: teleoperation (S1 · Twist 100 Hz), IMU (S2 · 200 Hz), a four-tier LiDAR ladder (S3 · 2D → VLP-16 → VLP-32C → HDL-64E / OS1-128), and a four-tier camera ladder (S4 · JPEG 720p → Depth 640×480 → RGB 720p → RGB 1080p).
Wired and Wi-Fi links, each of S1–S4 run independently. We measure Δ(TX+RX) added by the observer, plus drop rate and end-to-end latency (p50 / p95 / p99) on the observed topic.
SPDP and SEDP pub/sub packet counts per minute over a static N-node ROS 2 graph (N ∈ {2, 5, 10, 20, 50}). ros2probe contributes zero at steady state.
Four scenarios (idle · discovery-only observer · bag record · topic hz) × workloads {S1, S2, S3 64ch, S4 1080p} run independently × three hosts (Laptop, Orin Nano, Pi). pidstat CPU % and VmRSS.
Synthetic /stress topic swept across rate ∈ {10…1000} Hz × size ∈ {4 KB…16 MB} on laptop localhost. Drop-rate 1% saturation frontier compared head-to-head.
Inter-vendor graph reconstruction (FastDDS + CycloneDDS mixed), SHM-only topic visibility via rp discover, and loopback-interface auto-capture for intra-host UDP traffic.
Scripts talk to rp over a Unix socket. Humans get a GUI with a dashboard, a topic monitor, and a bag recorder.
rp gui → Dashboard
rp gui → Topic Monitor 1
rp gui → Topic Monitor 2
rp gui → Bag Recorder 1
rp gui → Bag Recorder 2
| Command | Description |
|---|---|
| rp run | Start the runtime daemon (auto-escalates to root if needed) |
| rp gui | Launch the desktop GUI |
| rp topic list | List topics with recordable / internal separation |
| rp topic hz <topic> | Live publish rate (mean / min / max / stddev) |
| rp topic bw <topic> | Live bandwidth |
| rp topic delay <topic> | End-to-end delay from header.stamp |
| rp topic echo <topic> | Stream decoded messages (rclpy-backed) |
| rp bag record [TOPICS…] | Record MCAP bag · zstd / lz4 / none |
| rp node list · info | Discovered nodes and their endpoints |
| rp discover | Force ros_discovery_info broadcast (SHM-aware shadow mode) |
Linux 5.15+, Rust stable + nightly, one eBPF linker. Takes about five minutes on a fresh Ubuntu.
Always required (TLS for reqwest):
sudo apt install -y pkg-config libssl-dev
Only for the full (GUI) build — file dialog via rfd/gtk3:
sudo apt install -y libgtk-3-dev
Install rustup, then add nightly + the eBPF linker:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" rustup toolchain install nightly --component rust-src cargo install bpf-linker
git clone https://github.com/csi-dgist/ros2probe.git
cd ros2probe
Full — runtime + CLI + GUI:
cargo install --path ros2probe --bins
Minimal — runtime + CLI only (no eframe/egui/rfd, no rp gui):
cargo install --path ros2probe --bins --no-default-features
| Requirement | Notes |
|---|---|
| Linux 5.15+ | eBPF socket filter, AF_PACKET TPACKET_V3 |
| ROS 2 Humble+ | Iron, Jazzy, Rolling also supported |
| Rust ≥ 1.85 | Stable + nightly (rust-src) — installed via rustup |
| bpf-linker | Required for the eBPF program |
| libssl-dev | Always required (reqwest TLS) |
| libgtk-3-dev | Full build only (rfd file dialog) · skip for minimal |
| Python 3 + rclpy | Only for rp topic echo / delay |
| Optional | Jumbo frames, DDS XML profile that avoids IP fragmentation |
Two build profiles.
Full = runtime + CLI + GUI (default).
Minimal = runtime + CLI only.
Use minimal on headless hosts; libgtk-3-dev is not needed.
BibTeX entry will be updated once the paper is accepted. Preprint citation below.
@inproceedings{ros2probe2026,
title = {ros2probe: Non-intrusive, Kernel-selective Observability for ROS 2 Middleware},
author = {Yu, Jisang and Lee, Sanghoon and Park, Kyung-Joon and others},
booktitle = {ArXiv},
year = {2026},
note = {In preparation}
}