Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
jazzy

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
kilted

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro lyrical showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro rolling showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro ardent showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro bouncy showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro crystal showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro eloquent showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro dashing showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro galactic showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro foxy showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro iron showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro lunar showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro jade showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro indigo showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro hydro showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro kinetic showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro melodic showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.
No version for distro noetic showing humble. Known supported distros are highlighted in the buttons above.
Repo symbol

ros2_pulse repository

ros2_pulse

ROS Distro
humble

Repository Summary

Checkout URI https://github.com/TanayK07/ros2_pulse.git
VCS Type git
VCS Version main
Last Updated 2026-08-24
Dev Status DEVELOPED
Released RELEASED
Contributing Help Wanted (-)
Good First Issues (-)
Pull Requests to Review (-)

Packages

Name Version
ros2_pulse 0.4.1

README

ros2_pulse

The heartbeat of your ROS 2 graph. A low-overhead probe that measures per-topic message rate and active-node liveness for both inter-process and intra-process traffic, on stock ROS 2 binaries, with no rebuild, no privileges and no network traffic. The counting hot path costs under a nanosecond per message; the whole probe costs about 2 % of workload CPU on a harsh 4,900 msg/s stress and less on real graphs.

CI ROS 2 License Docs

pulse-top: live terminal dashboard over the probe's jsonl log, catching a /scan stall and a /cmd_vel rate sag as they happen

pulse-top --demo: the probe’s log, live. Sparklines per topic, intra-process rates, structured warnings with ages. Install: pip3 install ros2-pulse-top (details).

40-second launch video: the cost of ros2 topic hz and echo, the one-line probe, pulse-top catching a stall, the measured numbers

Forty seconds on what watching a topic costs and what the probe does instead. Watch the video (MP4, 10 MB) or read the docs.


Why

The question in production is simple: is every topic flowing at the rate it should, and which nodes are alive? The existing tools each fall short of answering it.

  • ros2 topic hz and ros2 topic echo subscribe to one topic at a time. Watching a single 100 KB topic costs 7 % of a core with hz and 31 % with echo, they cannot see intra-process messages at all, and pointing hz at an intra-process topic makes the publisher start serializing every message, which raised the watched process’s CPU by 52 % in our measurements.
  • Built-in topic statistics are bypassed by intra-process comms on Humble-class binaries, so composable nodes carrying point clouds lose all introspection. This is fixed on rolling and newer by rclcpp#3130 (merged April 2026); there is no public Humble backport as of this writing.
  • ros2_tracing / LTTng is built for offline analysis: a session daemon plus post-processing of a CTF trace just to get a rate. On Humble it also needs ROS rebuilt with the lttng-ust backend (Jazzy and newer trace out of the box).
  • CARET (Tier IV) uses the same hook layer as this probe, LD_PRELOAD over tracetools, which is a useful independent validation of the mechanism. It is built for deep offline latency and chain analysis and needs LTTng, a forked rclcpp and Jupyter post-processing. Complementary, not always-on.
  • eBPF uprobes need CAP_SYS_ADMIN, debugfs and a kernel with BTF and uprobes, which is often a non-starter on Jetson and other embedded targets, and they pay a kernel trap per message.

ros2_pulse hooks the tracetools instrumentation layer that rclcpp already calls on every publish and every callback, counts in-process with a lock-free hot path, and writes ready-to-read Hz to a small rolling file.

What you get

# ts_ns=1782887153899445923 window_s=5.000
TOPIC /scan 20.000000                      # publish-side, inter-process
PUB   /points inter=0.000000 intra=30.000000   # publish-side incl. intra (Iron+)
RECV  /scan inter=20.000000 intra=0.000000 # receive-side, BOTH transports
RECV  /points inter=0.000000 intra=30.000000   # <- intra-process, invisible to other tools on Humble
JITTER /scan recv max_dt_ms=21.284             # largest inter-arrival gap (opt-in, see below)
NODE  /perception
NODE  /planner
WARN  TOPIC /scan hz=1.200000 expected=[18,22]  # only with an expected-rate spec (see below)

If a sidecar exporter or a log shipper is reading instead of a person, ROS_TOPIC_STATS_FORMAT=jsonl writes every window as one JSON object per line with the same gates and values. See JSON Lines output.

How it works

libros2_pulse.so is injected with LD_PRELOAD. It exports the same symbols as libtracetools.so’s tracepoint API (ros_trace_rcl_publish, ros_trace_callback_start, the init tracepoints and so on). The dynamic linker binds rclcpp’s calls to ours first; each interposer records a stat and forwards to the real function through dlsym(RTLD_NEXT, ...). rclcpp calls these functions unconditionally (the LTTng enable check is inside them), so the probe works with no tracing session and adds no DDS traffic.

  • Intra-process visibility comes from callback_start(callback, is_intra_process), which fires for every subscription callback regardless of transport.
  • The hot path is a per-endpoint relaxed atomic increment behind a 256-slot thread-local cache with a stride-breaking hash. No global lock, no per-message string hashing. Counting costs about 0.3 ns/op on a fixed endpoint and 0.6 to 1.2 ns/op alternating across a working set on the reference box, two orders of magnitude under a single LTTng-UST tracepoint (about 158 ns).
  • A background timer snapshots and resets the counts every ROS_TOPIC_STATISTICS_PUBLISH_PERIOD seconds and appends the rates to ROS_TOPIC_STATS_OUTPUT_FILE.

The pure C++ core (core/) has no ROS dependency and is unit tested on its own; the probe layer (probe/) is a thin LD_PRELOAD shim.

Install

cd ~/ros2_ws/src && git clone https://github.com/TanayK07/ros2_pulse.git
cd ~/ros2_ws && colcon build --packages-select ros2_pulse && source install/setup.bash

The pulse-top dashboard installs separately, no ROS environment needed:

File truncated at 100 lines see the full file

CONTRIBUTING

Contributing to ros2_pulse

Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome.

Ground rules

  • The core stays pure. include/ros2_pulse/core/ and src/core/ must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in src/probe/. This is what keeps the logic unit-testable in isolation.
  • The hot path stays cheap. onPublish / onCallbackStart run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only.
  • No DDS traffic, no privileges. The probe writes a local file and opens no sockets. Keep it that way.

Dev workflow

# unit tests (pure core, no ROS): the fastest loop
GT=/opt/ros/humble/src/gtest_vendor
g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \
  test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut

# full suite (unit + integration)
colcon build --packages-select ros2_pulse
colcon test --packages-select ros2_pulse && colcon test-result --verbose

# benchmark / overhead (see bench/README.md)

Pull requests

  • Add or update tests for any behavior change (test/unit for core, test/integration for the probe end-to-end).
  • Run colcon test and include the result in the PR.
  • For performance-affecting changes, run bench/run_overhead_repeated.sh and paste the numbers.
  • Keep commits conventional (feat:, fix:, perf:, docs:, test:).

Reporting bugs

Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant pulse.log window, and nm -D libtracetools.so | grep -c ros_trace output.

# Contributing to ros2_pulse Thanks for your interest. This is a small, focused package; contributions that keep it small and focused are the most welcome. ## Ground rules - **The core stays pure.** `include/ros2_pulse/core/` and `src/core/` must not depend on ROS, rclcpp, or tracetools. All ROS/tracetools coupling lives in `src/probe/`. This is what keeps the logic unit-testable in isolation. - **The hot path stays cheap.** `onPublish` / `onCallbackStart` run per message. No allocations, no string hashing, no global locks, per-endpoint atomics + the thread-local cache only. - **No DDS traffic, no privileges.** The probe writes a local file and opens no sockets. Keep it that way. ## Dev workflow ```bash # unit tests (pure core, no ROS): the fastest loop GT=/opt/ros/humble/src/gtest_vendor g++ -O2 -std=c++17 -pthread -Iinclude -I$GT/include -I$GT \ test/unit/test_topic_registry.cpp src/core/topic_registry.cpp $GT/src/gtest-all.cc -o /tmp/ut && /tmp/ut # full suite (unit + integration) colcon build --packages-select ros2_pulse colcon test --packages-select ros2_pulse && colcon test-result --verbose # benchmark / overhead (see bench/README.md) ``` ## Pull requests - Add or update tests for any behavior change (`test/unit` for core, `test/integration` for the probe end-to-end). - Run `colcon test` and include the result in the PR. - For performance-affecting changes, run `bench/run_overhead_repeated.sh` and paste the numbers. - Keep commits conventional (`feat:`, `fix:`, `perf:`, `docs:`, `test:`). ## Reporting bugs Open an issue with: ROS distro, RMW (FastRTPS/CycloneDDS), whether intra-process comms are on, the relevant `pulse.log` window, and `nm -D libtracetools.so | grep -c ros_trace` output.