Skip to content
| Marketplace
Sign in
Visual Studio Code>Visualization>pauli's ROS DarshanNew to Visual Studio Code? Get it now.
pauli's ROS Darshan

pauli's ROS Darshan

Piyush

| (0) | Free
Live observability for ROS 2 — particle-flow node/topic graph and executor visualization, right inside VSCode.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

ROS Darshan

Observe. Understand. Debug.

A live observability tool for ROS 2. ROS Darshan connects to a running ROS system, discovers everything automatically, and shows a live communication graph where animated particles represent actual message flow — particle count and speed track real topic frequency. On top of that graph, an executor visualization answers a different question than "who talks to whom": what is every node doing right now — waiting, executing a callback, backed up with ready work, or running long — with mock/placeholder runtime events wired all the way through so the feature is fully demoable before any real ROS 2 instrumentation exists. See Runtime / Executor Visualization below.

What the MVP includes

  • Automatic discovery of nodes and topics (polled every second, no manual refresh)
  • Live Node → Topic → Node graph with automatic layout
  • Particle-flow edges — the signature feature. 0 Hz means no particles and a dashed edge; 100 Hz looks like a continuous stream
  • Per-topic live stats: frequency and bandwidth
  • Clickable inspector for nodes (publishers / subscribers / services) and topics (type, Hz, bandwidth, pubs/subs, live message preview)
  • Search with graph highlighting
  • Rule-based diagnostics: subscribers with no publisher, published-but-unheard topics, connected-but-silent (0 Hz) topics
  • Mock mode: a simulated mobile robot so you can run and demo everything on a machine with no ROS installed
  • Executor visualization (Mode 2, mock-driven today): expand any node to see its live executor state, current callback with a progress bar, ready-queue contention, and running statistics (utilization, callback counts, avg/max duration, messages in/out)

Quick start (no ROS required — mock mode)

Terminal 1 — backend:

cd backend
pip install -r requirements.txt
python -m ros_darshan.main --source mock

Terminal 2 — frontend:

cd frontend
npm install
npm run dev

Open http://localhost:5173. You should see a simulated robot graph with particles flowing. Every ~12 seconds a /debug_introspector node joins and leaves the graph to demonstrate live discovery. /imu/data is intentionally dead (0 Hz) so you can see the stalled state and the diagnostics panel (⚠ button, top right).

Click any node (or hit the ▸ caret on its header) to expand it and watch its executor: a state dot (green waiting, blue executing, red executing-long), the current callback's progress bar, and its ready queue. /slam_toolbox's scan callback occasionally runs long (>150 ms) to show the red "exceeds threshold" state; /object_detector's camera callback visibly triggers a /detections publish (watch the edge flash) to show the callback → publish chain in action.

Connecting to a real ROS 2 system

On a machine with ROS 2 (Humble or newer) and your robot running:

source /opt/ros/humble/setup.bash   # or your distro
cd backend
pip install -r requirements.txt
python -m ros_darshan.main --source ros2

Then start the frontend as above. Notes:

  • Frequency and bandwidth are measured with raw (serialized) subscriptions — messages are never deserialized for stats, so overhead stays low even on image topics.
  • Message previews deserialize only the topic currently open in the inspector, and are truncated (long arrays cut to 8 elements) before being sent to the UI.
  • Stats subscriptions use best-effort QoS. Reliable-only publishers will still appear in the graph but may read 0 Hz; QoS-aware matching is on the roadmap.
  • If the frontend runs on a different machine, point it at the backend with http://<frontend-host>:5173/?ws=ws://<backend-host>:8765/ws.

Run as a VSCode extension

The repo root is also a VSCode extension: it bundles the built frontend (frontend/dist) and the backend Python source into a webview panel, and manages the backend process for you — no separate terminals needed.

Run it from source (dev loop):

cd frontend && npm install && npm run build   # produces frontend/dist
cd ..         && npm install && npm run compile

Open the repo root in VSCode and press F5 (uses the included .vscode/launch.json) to launch an Extension Development Host, then run "ROS Darshan: Open Graph View" from the Command Palette. The backend starts automatically (mock source by default) on an auto-picked free port; its logs go to the ROS Darshan Output channel.

Settings (Preferences: Open Settings, search "ROS Darshan"):

  • rosDarshan.source — mock (default) or ros2.
  • rosDarshan.pythonPath — interpreter to run the backend with (defaults to python3 / python on Windows). Point this at a venv with backend/requirements.txt installed.
  • rosDarshan.ros2SetupScript — path to a ROS 2 setup script (e.g. /opt/ros/humble/setup.bash) to source before launching the backend when source is ros2. Linux/macOS only.

If the backend's dependencies aren't installed, the extension detects the failure and offers a one-click Install Requirements action. Changing settings doesn't restart the backend automatically — use "ROS Darshan: Restart Backend" (or the notification's "Restart Now" button) to apply changes.

Package it:

npx vsce package
code --install-extension ros-darshan-0.1.0.vsix

This produces a self-contained .vsix — the frontend build and backend source travel with it, so the only runtime requirement on the installing machine is a Python interpreter with backend/requirements.txt installed (and, for ros2 mode, a sourced ROS 2 environment).

Architecture (MVP + runtime layer)

backend/
  ros_darshan/
    main.py            CLI entrypoint (--source mock|ros2)
    server.py          FastAPI + WebSocket broadcast loop (graph + runtime)
    models.py          NodeInfo / TopicInfo / EdgeStats / GraphSnapshot
    sources/
      base.py          GraphSource interface (the seam for future sources:
                       rosbag playback, MCAP, remote agents)
      mock_source.py   simulated robot for development and demos
      ros2_source.py   rclpy adapter: discovery polling + raw-subscription stats
    runtime/           runtime/executor layer — independent of sources/ above
      models.py        RuntimeEvent, CallbackEntry, NodeRuntimeState, ...
      source.py        RuntimeSource interface (the seam for a future
                       instrumented source — see "Instrumentation" below)
      aggregator.py     RuntimeAggregator: RuntimeEvent stream -> per-node
                       state + rolling stats (utilization, avg/max duration)
      mock_runtime.py  MockRuntimeSource: simulates per-node executor
                       activity from the same NODES/TOPICS tables
                       mock_source.py uses, so the two mocks stay consistent
frontend/
  src/
    ws.js              WebSocket client with exponential-backoff reconnect
    store.js           zustand store + diagnostics rules + runtime slice
    layout.js          dagre left-to-right auto layout (reserves extra
                       height for expanded nodes)
    components/
      GraphView.jsx    React Flow canvas
      GraphNodes.jsx   custom node renderers (ROS node w/ executor state dot
                       + expand caret, topic pill with Hz badge)
      ExecutorPanel.jsx state/current-callback/ready-queue view, shared by
                       the inline canvas panel and the Inspector
      ParticleEdge.jsx signature particle-flow edge (respects
                       prefers-reduced-motion) + publish-flash overlay
      Inspector.jsx    node/topic inspector — Executor + Statistics
                       sections added for nodes
      Chrome.jsx       top bar (search, diagnostics, connection) + status bar

The runtime/ package and sources/ package never import each other — server.py is the only place both are known, and only as interfaces (GraphSource, RuntimeSource). That's what makes it possible to run with a graph source and no runtime source at all (real ROS 2 today), or swap the mock runtime source for a real one later, without touching graph code or any frontend file.

WebSocket protocol

// server -> client
{"type": "snapshot", "nodes": [...], "topics": [...]}   // on connect + on graph change
{"type": "stats", "topics": {"/scan": {"hz": 9.8, "bandwidth_bps": 78400}}}  // every 500 ms
{"type": "message", "topic": "/odom", "preview": {...}} // only while inspecting
{"type": "capabilities", "runtime_available": true}     // once, right after snapshot
{"type": "runtime", "nodes": {"/lidar_driver": {         // every ~150 ms, ALL nodes, cheap
   "state": "executing", "utilization": 0.42, "callback_count": 812,
   "avg_ms": 3.1, "max_ms": 118.4, "messages_in": 812, "messages_out": 812}}}
{"type": "runtime_detail", "node": "/lidar_driver", "detail": {  // every ~150 ms,
   ...same fields as above, plus:                                // only EXPANDED nodes
   "ready_queue": [{"name": "rosout_timer", "kind": "timer", "state": "waiting", ...}],
   "current": {"name": "scan_timer", "kind": "timer", "elapsed_ms": 12.4, "expected_ms": 2.3}}}

// client -> server
{"type": "subscribe", "topic": "/odom"}
{"type": "unsubscribe", "topic": "/odom"}
{"type": "expand_node", "node": "/lidar_driver"}     // start receiving runtime_detail for it
{"type": "collapse_node", "node": "/lidar_driver"}   // stop

The runtime/runtime_detail/capabilities messages only ever appear if the backend was started with a RuntimeSource (mock mode always has one; --source ros2 does not yet — see below). The frontend treats their absence as a fully supported mode, not an error: the Inspector's Executor section shows a one-line "needs an instrumented source" note instead.

Runtime / Executor Visualization

What ROS 2 gives us directly (Mode 1 — discovery, already working)

ros2_source.py uses rclpy's graph introspection API to get node names/namespaces and publisher/subscriber/service names+types per node, and attaches raw (serialized) subscriptions itself to measure Hz/bandwidth. All of this is passive observation of the ROS graph — it needs no cooperation from the nodes being watched.

What discovery cannot give us, because it never crosses the ROS graph — it only ever exists inside each node's own process memory: executor state (spinning/blocked), which callback is currently running, the ready/wait queue, per-callback duration, thread id. rclpy/rcl do not publish any of this over the graph.

What requires instrumentation (Mode 2)

Three approaches, in order of general applicability:

  1. ros2_tracing / LTTng tracepoints — the "official" ROS 2 answer. Low overhead, but the target process must be launched with tracing enabled, plus a kernel/userspace tracing toolchain on the robot; rclpy tracepoint coverage is thinner than rclcpp's. A heavier ops lift, best suited to a later "production tracing" mode.
  2. An opt-in instrumentation helper the node author imports (the recommended next step) — a small ros_darshan_instrument helper wrapping a node's create_subscription/create_timer/create_service calls and its executor callback invocation, publishing a JSON RuntimeEvent (same shape as runtime/models.py) onto a side-channel topic like /_darshan/<node>/runtime_events (plain std_msgs/String, no custom .msg needed). Ros2Source would subscribe to any topic matching that pattern and feed events into the same RuntimeAggregator the mock uses today. One import, no code restructuring, works with stock rclpy.
  3. In-process executor monkey-patching — only works for nodes launched inside our own process, so it can't attach to whatever's already running system-wide. Not pursued as a general solution.

This pass implements the seam so option 2 is a drop-in later: any future source only needs to produce RuntimeEvents and feed them to RuntimeAggregator — server.py's broadcast logic and every frontend file are already generic over "is there a RuntimeSource or not."

Mock runtime model

MockRuntimeSource derives callback schedules from the same NODES/TOPICS tables mock_source.py already uses for the graph and particle stats, so the executor view stays visually consistent with the particle animation. Deliberate simplifications:

  • Every node behaves like a SingleThreadedExecutor: one callback at a time; anything that becomes ready while busy is queued and shown as ready until its turn — this is what produces the ready-queue contention you'll see on busier nodes.
  • Only one causal publish chain is modeled explicitly: /object_detector's camera-subscription callback is the one that emits the /detections publish (it has exactly one publisher and one subscriber, so the link is unambiguous). Every other node's publish comes from its own timer-driven callback. This is enough to make "message arrives → callback executes → publish happens" visible without inventing precise multi-topic causality data that doesn't exist elsewhere in the mock.
  • No action callbacks are simulated — the existing mock graph never populates NodeInfo.actions, so there's no data to hook into yet.
  • /slam_toolbox's scan callback has a small chance of running long (180–320 ms) to exercise the red "executing_long" state, the same way mock_source.py keeps /imu/data permanently at 0 Hz to exercise the stalled-topic case.

Frontend design decisions

  • Two broadcast tiers, not one. All nodes get a cheap runtime pulse (state + a handful of numbers) every tick regardless of graph size; only nodes the user has expanded get the fuller runtime_detail (ready queue, current callback). This mirrors the existing subscribe/unsubscribe pattern already used for topic message previews, and is what keeps the default view's bandwidth flat no matter how many nodes exist — "don't overload the default view" from the brief, enforced structurally rather than by convention.
  • Snapshots over raw event streaming to the browser. The backend's RuntimeAggregator still consumes a genuine RuntimeEvent stream internally (wake/sleep/callback start/end/publish — the literal "Runtime Event Stream" layer), but the wire protocol to the browser exposes aggregated, poll-friendly snapshots at ~150 ms instead of raw discrete events. This is simpler and more robust on the frontend (replace state each tick, no client-side reducer reconstructing executor state from a partial event log) while still reading as continuously animated.
  • Canvas expansion is deliberately minimal; the Inspector carries the rest. Clicking a node auto-expands it in place on the canvas (the caret is a secondary control for expanding a second node without losing your current Inspector selection) — but only the "alive" bits (state dot, current-callback progress bar, a truncated ready queue) render inline. Full statistics and the un-truncated ready queue live in the existing Inspector sidebar. Putting everything the brief's mock sketch shows (Timers/Publishers/Subscribers/Services/Actions/Statistics, in full) directly on the canvas would violate the brief's own "keep the graph clean" / "remain responsive with many nodes" principles once a node has a dozen callbacks — so canvas vs. sidebar is a split by how glanceable vs. how detailed, not a smaller version of the same view.
  • Publishing is an edge effect, not a node state. An earlier version of the executor-state derivation treated "just published" as a fourth node color, but topics publishing faster than ~4 Hz (most of them) never let that flash window expire, so busy nodes never looked idle. Publishing now only flashes the edge of the topic that was just published to (ParticleEdge's glow overlay) — accurately representing that publishing is something a callback does while executing, not a distinct executor state.

Known limitations (deliberate MVP scope)

  • Services are listed in the inspector but not drawn in the graph; actions and TF are not yet discovered.
  • No specialized message viewers yet (Image, PointCloud2, ...) — previews are structured JSON.
  • Layout recomputes on every graph change; on very large graphs (500+ topics) this should move to incremental layout and edge virtualization.
  • Snapshot diffing is whole-snapshot; a delta protocol is the natural next step.
  • Not yet packaged as a desktop app — it runs as a local web app. Electron/Tauri packaging is roadmap work (Tauri recommended: ~10x smaller binaries).
  • Runtime/executor visualization is mock-only. --source ros2 runs in Mode 1 (discovery only) today; no RuntimeSource is wired to real ROS 2 yet. See "What requires instrumentation" above for the recommended next step and the wire format it should produce.
  • Ready-queue depth is only ever an estimate of scheduled-but-not-yet-run callbacks we know about; true executor wait-set queue depth isn't exposed by rclpy even with instrumentation, without patching the executor itself.
  • No action callbacks are simulated in the mock (no action data exists elsewhere in the mock graph to derive schedules from).

Before you take this public

Suggested checklist:

  1. Test against a real robot or ros2 launch turtlebot3_gazebo ... / demo_nodes_cpp talker and file what breaks.
  2. Add a LICENSE (Apache-2.0 is the ROS-ecosystem norm), CONTRIBUTING.md, and issue templates.
  3. Pin versions, add CI (lint + frontend build + backend unit tests on the mock source).
  4. Record a GIF of the particle flow for the README — it is the whole pitch.

License

Choose one before publishing. Apache-2.0 recommended for ROS-ecosystem compatibility.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft